XTab's Blog

Ged Mead's Blog at vbCity

vbCity Blogs moved to:
http://cs.vbcity.com/blogs
  Home :: Syndication  :: Login

AprMay 2013Jun
SMTWTFS
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

Archives

Topics

Ramblings

VB.NET

In the previous blog, I created a simple ValueConverter that analyzed the data bound values and set the Foreground color of a TextBlock based on whether the value was less or more than a cut off value of 200 units. In reality, these preset values (such as the 200 in that demonstration) are rarely that rigid and you often need some flexibility in the analysis. To continue the example of a collection of drink products, it might be more realistic to compute a cut off value on the fly. For instance, if you know your annual sales for an individual item and you know the quantity you have in stock, you might want to highlight those items that need to be re-ordered. If it's a big selling item, the value of 200 may be far too low as a cut off; conversely an item with low annual sales might only need to be re-ordered when the stock is down to a handful.

So, how do we include this kind of calculation in a process where the value is data bound, but we need to apply some arithmetic to more than one field? The answer is to use MultiBinding and a MultiValueConverter.

You create a class that implements IMultiValueConverter. The core difference between this class and the one created in the previous blog is that the 'value' parameter of the Convert method is renamed to 'values' and is an array, not a single object. Armed with that array, you can pass in the values of two or more of the fields of your data source, run the calculations and Return a result.

Take a look at this class:

Public Class StockLevelConverter

    Implements IMultiValueConverter

 

    Public Function Convert(ByVal values() As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert

 

        If targetType IsNot GetType(Brush) Then Return Nothing

 

        '  Variables for the arithmetic operations

        Dim QtyHeld As Integer = CInt(values(0))

        Dim AnnualSales As Integer = CInt(values(1))

 

        '  Avoid divide by zero errors by not proceeding to the calculation

        '  and returning a different brush

        If QtyHeld = 0 Or AnnualSales = 0 Then Return Brushes.DarkGray

 

        Dim RestockLevel As Integer = AnnualSales / 12

 

        Return (If(QtyHeld < RestockLevel, Brushes.Red, Brushes.MediumBlue))

 

    End Function

 

    Public Function ConvertBack(ByVal value As Object, ByVal targetTypes() As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack

        Throw New NotImplementedException()

    End Function

End Class

It is similar to the converter class created in the previous blog, having the same two methods, Convert and ConvertBack.    Note that this class implements IMultiValueConverter and the first parameter being an array as mentioned earlier.

In the Convert method, I have included the test that confirms that the correct Target Type has been passed in. Two variables, QtyHeld and AnnualSales are created and you will see that their respective values are taken from the two elements of the array named 'values' passed in as a parameter. We will need to look at how those array elements are notified to the converter in a moment.

Continuing with the Convert method, a test is included to avoid any divide by zero errors. I have chosen to Return a different colored Brush, but it could have been left to the default of MediumBlue if preferred.

The 'RestockLevel' variable could have been left out and the calculation that it uses could have been placed inside the If statement in the final line. But I felt that it is easier to read in the more verbose approach. The gist of these two lines is to divide the annual sales figure by 12 in order to find the average monthly sales. If the Stock held is less than one month's worth, then a Red Brush is returned, otherwise a MediumBlue one.

So, how do the values in the array get passed in to the Convert method? The answer is that a MultiBinding is used in the XAML. This is a similar approach to the single ValueConverter example seen previously, except that this time two Paths are required - one for the Quantity field and one for the AnnualSales field of the data source.

Assuming you are using the same project as in the previous blog, there is already a namespace mapping named 'local'. You then need to create an instance of this new converter class:

    <local:StockLevelConverter x:Key="StockChecker" />

The DataTemplate for the Quantity column cells will contain the MultiBinding. Here is the markup for this: 

    <DataTemplate x:Key="QtyCellTemplate">

      <TextBlock

          Text="{Binding Path=Quantity}" >

        <TextBlock.Foreground>

          <MultiBinding Converter="{StaticResource StockChecker}">

            <Binding Path="Quantity" />

            <Binding Path="AnnualSales" />

          MultiBinding>

        TextBlock.Foreground>

      TextBlock>

    DataTemplate> 

The Element.Property syntax is used to allow for the multiple sub elements. The MultiBinding is created and set to point to that instance of the StockLevelConverter class, which has been keyed as 'StockChecker'. The two paths which are passed in to the StockLevelConverter are listed next. Note that the array is filled in the order in which these paths are listed - that is values(0) will take the Quantity field values and values(1) will take the AnnualSales field values.

That's all there is to it. If I create some arbitrary dummy data for all the properties of the DrinkProduct class collection (i.e. including the Quantity and AnnualSales figures) and run the project, the resulting ListView will look like this:

 

If you're interested in trying this out, using the class code from the previous blog but can't be bothered to create your own dummy data, here is the collection I created:

 

  Public Shared Function StockCheck() As List(Of DrinkProduct)

        Dim CurrentProducts As New List(Of DrinkProduct)

        With CurrentProducts

            .Add(New DrinkProduct("CF1kg", "Coffee Powder", "1 Kg", MaterialType.Powder, 15684, 1276))

            .Add(New DrinkProduct("CFB500", "Ground Coffee", "500 g", MaterialType.Powder, 22785, 12856))

            .Add(New DrinkProduct("CFG500", "Coffee Granules", "500 g", MaterialType.Granules, 19233, 5907))

            .Add(New DrinkProduct("Te500", "Tea", "500 g", MaterialType.Leaf, 8544, 235))

            .Add(New DrinkProduct("TeInst500", "Instant Tea", "500 g", MaterialType.Powder, 1009, 22))

            .Add(New DrinkProduct("SMlk1lt", "Skimmed Milk", "1 Litre", MaterialType.Liquid, 28012, 2650))

            .Add(New DrinkProduct("HiJ300", "HiJuice Drink Mix", "300 g", MaterialType.Other, 578, 179))

            .Add(New DrinkProduct("Sm400", "Smoothie", "400ml", MaterialType.Paste, 9346, 3284))

            .Add(New DrinkProduct("Beef300", "Beef Drink", "300 g", MaterialType.Granules, 8316, 1965))

            .Add(New DrinkProduct("Beef750", "Beef Drink", "750 g", MaterialType.Granules, 7612, 359))

 

        End With

 

        Return CurrentProducts

    End Function

 

Don't forget to include the DataContext in the code-behind of Window1, the Window that holds the actual ListView:

Class Window1

    Dim CurrentProducts As New List(Of DrinkProduct)

 

 

    Private Sub Me_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        CurrentProducts = DrinkProduct.StockCheck()

        Me.DataContext = CurrentProducts

    End Sub

End Class

posted on Sunday, October 25, 2009 1:18 PM