O'Reilly logo

Data-Driven Services with Silverlight 2 by John Papa

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Managing Lists, Templates, and Converters

Silverlight 2 supports binding lists of items to target controls through the same mechanisms as those you would use to bind to a single item, such as an entity. Binding to lists also is similar to binding to single entities. Entities can implement the INotifyPropertyChanged interface to communicate with the target controls to notify them when a property value has changed. List-based controls can also benefit from notifications if they are bound to lists created with the ObservableCollection<T> or collection classes that implement the INotifyCollectionChanged interface.

You can bind controls directly to an object source or to a list of objects. This is especially useful for binding a list of objects to a list-based control such as ItemsControl, ListBox, or DataGrid, or to a third-party list-based control. Like the INotifyPropertyChanged interface, the INotifyCollectionChanged interface provides a way for the collection to notify the bindings when the contents have changed.

List-based controls in Silverlight 2 have an ItemTemplate property that defines how each row in a list-based control will be presented. The ItemTemplate can refer to a DataTemplate resource, which you can then reuse to apply the same template to different list-based controls. The template’s contents can be data-binding targets so that they can benefit from the data-bound source’s property values. This chapter will build on the binding techniques we’ve already discussed, and will show you how to bind to lists of entities, implement event notifications, and design item templates for lists.

Binding to List-Based Controls

List-based binding begins with a list of items. These items can be represented by, for example, a List<T> or any IEnumerable derived class. Once the list of items is obtained, the list can be bound to an appropriate target control such as a ListBox, ItemsControl, ComboBox, or DataGrid.

Note

Because the concepts of binding apply to all of these types of list-based controls in much the same way, the examples in this chapter will use the ListBox control. ItemsControl is similar to ListBox, which inherits from ItemsControl.

The ListBox control can display items in a standard and straight list, or it can have its item template completely overridden and replaced with a custom set of XAML. Either way, a list of items that is bound to the ListBox control can be displayed item by item within the ListBox.

Setting the ItemsSource

Figure 4-1 shows a basic ListBox bound to a list of products. This ListBox is bound by setting the ListBox control’s ItemsSource property to the List<Product>. The previous examples in this book bind a property of a target control to a source object’s property. In the examples, the source object is set to the DataContext (either directly or through an inherited DataContext). The target controls in the previous examples had a value that they needed to display, and that value was derived from the data-binding source. For example, a TextBox control that displays a product’s name from an entity has the TextBox’s Text property bound to the source. A ListBox control differs from these examples in that the ListBox will display several items, and binding the source to a single property of the ListBox (the ItemsSource) is not enough to make the ListBox display the item values.

ListBox bound to basic list
Figure 4-1. ListBox bound to basic list

The ListBox control in Figure 4-1 has its ItemsSource property bound to the DataContext. This associates the List<Product> to the ListBox, but the display of the items must also be set up. Otherwise, if the ItemsSource is bound and the contents of the ListBox are not established, the ListBox will display the ToString() contents of each item in the List<Product>, as shown in Figure 4-2.

ListBox bound to ItemsSource
Figure 4-2. ListBox bound to ItemsSource

The ListBox shown in Figure 4-2 does not display the contents of each item, because it is not being told what to display or how to display it. The “how to display it” part is determined by creating controls that will visually display the desired content. In this case, the ListBox’s items can simply be displayed as a TextBlock that shows the name of each product. The display could also be much more elaborate, as you can represent the content of each ListBox item using templates of your own design. For the example shown in Figure 4-1, the ListBox items (displayed via the ItemTemplate property) are represented by a DataTemplate that contains a single TextBlock.

Example 4-1 shows the XAML for the ListBox shown in Figure 4-1. Two main bindings are functioning here. First, the List<Product> is bound to the ListBox’s ItemsSource property. Second, each row in the ListBox is bound to a Product from the ItemsSource binding and will be displayed using the DataTemplate.

Example 4-1. ListBox with a template
<ListBox x:Name="lstProducts" Height="88" HorizontalAlignment="Left"
    VerticalAlignment="Bottom" Margin="20,20,20,5" Width="440"
    Style="{StaticResource ListBoxStyle}" ItemsSource="{Binding}" >
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding ProductName, Mode=OneWay}"
                Style="{StaticResource TextBlockCaptionStyle}"
                FontSize="14"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Notice that the TextBlock is using data binding to bind the Text property to the ProductName property of the binding source. The binding source refers to the objects in the ItemsSource, which gets the data source from the DataContext. The DataContext will be set at runtime in the .NET code to a List<Product> which is bound to the ItemsSource property of the ListBox. Each row in the ListBox is bound to one instance of a Product. This symbiotic relationship between the source object, the DataContext, and the bound target control properties enables developers to declare what will be bound, and how, at design time in the XAML.

The ListBox control’s ItemsSource shown in Example 4-1 is set to the Binding keyword. This indicates that the ItemsSource will be bound to the inherited DataContext. You could set the DataContext for the ListBox or a parent control of the ListBox to the List<Product>, and the ItemsSource would inherit that List<Product>. The code in Example 4-2 shows how to do this inside the Loaded event handler for the Silverlight control.

Example 4-2. Setting the DataContext
C#
List<Product> productList = CreateProductList();
lstProducts.DataContext = productList;
VB
Dim productList As List(Of Product) = CreateProductList()
lstProducts.DataContext = productList;

Note

The CreateProductList method creates a List<Product> inside the Silverlight 2 control. Chapters 511 will demonstrate how to retrieve records and fill them from server-based services using Windows Communication Foundation (WCF) and REST. You can find the source for CreateProductList and all of the source code for this chapter in the sample code in the ListBindings solution.

An alternative method is to set the ItemsSource directly in the code. Either way, the ListBox gets the collection of items, which then leaves it up to the template to determine how to display the Product entity information. The code in Example 4-3 shows how to do this inside the Loaded event handler for the Silverlight control.

Example 4-3. Setting the ItemsSource directly
C#
    List<Product> productList = CreateProductList();
    lstProducts.ItemsSource = productList;
VB
    Dim productList As List(Of Product) = CreateProductList()
    lstProducts.ItemsSource = productList

Whatever binding technique you use, it is important to be consistent throughout the application. For example, it is a poor coding practice to bind an ItemsSource property to the DataContext in one place and to bind an ItemsSource property directly to a specific List<T> someplace else. Consistency is a good rule to stick with in development. However, it could be argued that using the DataContext technique is a better approach, as all other nonlist-based bindings rely on it as well.

Binding Mode Considerations

Notice that the binding mode for the TextBox shown in Example 4-1 is set to OneWay. This indicates that the TextBox will listen for notifications that tell it when the value that it is bound to changes. This means that if the value changes in the source object, that new value will be presented in the TextBox immediately.

Consider that this application is extended so that a set of TextBox controls below the ListBox will represent the details of each product. These TextBox controls that represent the details are bound to the selected Product using the TwoWay binding mode. If you want the changes a user types to automatically update the ListBox contents, OneWay does the job. However, if you want the changes to never be updated in the ListBox, the OneTime binding mode might be better suited to this task.

Choosing the appropriate binding mode is important and depends on the application’s requirements. A good rule of thumb to follow is to set the binding mode for ListBox items to OneTime when dealing with most master detail scenarios. Also, you should use OneWay binding for ListBox items only when you need to have the items updated, as the values in the source object change, which may be when another control loses focus. Often, ListBox items or DataGrid items are interpreted as representing what has come out of a data source, such as a database. Thus, if a user changes a value and has not yet clicked Save, and the value is changed in the ListBox (due to OneWay binding in the ListBox items), the user could become confused.

Templates and Rows

You can add items to ListBox controls declaratively, through code, or through data binding.

Adding ListBox items through declarative techniques requires simply adding items in the XAML either by typing them in or by using a designer such as Expression Blend. But although the declarative technique makes it easy to add items in a designer, it does not offer a way to add items from a database, because the values are not known at design time.

To set or modify items through code, you would access the object model for the ListBox. You would use this technique when employing a manual binding strategy. Manual binding involves pulling values from a data source and manually loading them into target controls in the ListBox. Then the values can be manually pulled from the controls and back to a data source when the user clicks a button, for example. Manual binding does not require a DataContext or any binding syntax. Instead, it simply loads property values into targets, one by one, for each row in the ListBox.

A ListBox can take advantage of data-binding techniques to eliminate the code that is required in the manual binding process. Using the binding syntax and the techniques shown in the previous examples in this chapter, you can easily make a ListBox push and pull data to and from a data source and a target.

The ListBox control uses a template to predetermine the layout of its item rows. You define this template using the ItemTemplate of the ListBox. The ItemTemplate can refer to a DataTemplate that defines the contents of the bound elements that you will use to present the values from the data-binding source. You can define the ItemTemplate directly inline, as shown in Example 4-1, or it can refer to a DataTemplate as a resource. Either way, the DataTemplate is a very useful technique for designing repetitive rows of information for controls such as the ListBox.

DataTemplates As Resources

Instead of creating a DataTemplate inline, you can create it as a resource. This approach allows you to use throughout the Silverlight 2 application the layout you used in the DataTemplate. For example, you could create a user control in the application that defines the layout of the same controls you will be using in a list-based control. You can reference this new user control as the DataTemplate source anywhere within the Silverlight 2 application. In this case, a ListBox control can refer to the resource by its Key property. You can remove the DataTemplate in Example 4-1 and create it as a resource, as shown in Example 4-4. The DataTemplate syntax itself is exactly the same, except for the addition of the x:Key attribute. The value of the Key property is referred to by the ItemTemplate, so it can use the exact DataTemplate it requires.

Example 4-4. DataTemplate as a resource
<UserControl.Resources>
    <DataTemplate x:Key="ProductTemplate">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding ProductName, Mode=OneWay}" 
Style="{StaticResource TextBlockCaptionStyle}" FontSize="14"/>
        </StackPanel>
    </DataTemplate>
</UserControl.Resources>

You define the DataTemplate within the UserControl’s resource section, though you also could create it in any resource section the ListBox can obtain. For example, you could have created it as a resource of the container control of the ListBox, which in this case is a StackPanel. Creating the DataTemplate as a resource helps separate the declarative code and allows the template to be used by other controls, if needed. You can also create the DataTemplate as a resource in the app.xaml page. This would allow the template to be accessible from anywhere within the Silverlight application.

Changing an inline DataTemplate to a resource makes the ListBox control’s XAML much cleaner. The ListBox control shown in Example 4-1 shows several lines of XAML that include the reference to the DataTemplate via the ListBox’s ItemTemplate. By removing all of the children elements of the ListBox element shown in Example 4-1, you could rewrite this as a single line of XAML.

Example 4-5 shows the modified version of this XAML. The ItemTemplate property is moved up into the ListBox element and refers to a StaticResource named ProductTemplate. You can replace the XAML in Example 4-1 with the combination of the XAML in Examples 4-4 and 4-5 to yield the same effect shown in Figure 4-1.

Example 4-5. ListBox referring to a DataTemplate resource
<ListBox x:Name="lstProducts" Height="88" HorizontalAlignment="Left"
    VerticalAlignment="Bottom" Margin="20,20,20,5" Width="440"
    Style="{StaticResource ListBoxStyle}"
    ItemsSource="{Binding}"
    ItemTemplate="{StaticResource ProductTemplate}"/>

Complex DataTemplates

DataTemplates do not need to contain merely columns of information. Instead, they can contain any creative combination of elements that are required to present the data. For example, you could define a DataTemplate as a Grid container control that contains a table of values, or a Canvas layout control that contains images and text from the data source, or a StackPanel layout control with a series of horizontally and vertically stacked controls.

Figure 4-3 shows a ListBox that uses a DataTemplate that contains a slightly more complicated presentation. The template contains a horizontally oriented StackPanel, which contains a nested, vertically oriented StackPanel and a Grid layout control, for a side-by-side effect. The nested StackPanel contains two TextBlocks that are used to display the ProductName and UnitPrice properties from the data source. These TextBlocks use some custom styles to give them the gradient effect shown in Figure 4-3. The Grid layout control appears to the right. It contains three rows of property names and values, which display a summary of the product information. This is a very simple demonstration of how to create DataTemplates that contain nested controls for presentation. You can modify this in many ways and with a variety of controls to present a more aesthetically pleasing look based on the application.

Note

The UnitPrice shown in Figure 4-3 is not formatted as a currency value. The value for UnitPrice is a decimal value that must be converted to represent the currency value. You can do this using a converter class, which I will explain later in this chapter.

ListBox’s DataTemplate with nested controls
Figure 4-3. ListBox’s DataTemplate with nested controls

The XAML that creates the ListBox shown in Figure 4-3 appears in Example 4-6. All of the styles are defined as local resources to the Silverlight control named ListBoxTemplate. Notice that the DataTemplate ListBoxTemplate is contained as a local named resource and that the ListBox lstProducts references the DataTemplate in its ItemTemplate property.

Example 4-6. Stacked DataTemplate
<UserControl x:Class="ListBindings.ListBoxTemplate"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="510" Height="500">
    <UserControl.Resources>
        <Style x:Key="TitlePanel" TargetType="StackPanel">
            <Setter Property="Margin" Value="3,3,3,3"/>
            <Setter Property="Width" Value="260"/>
            <Setter Property="Height" Value="50"/>
            <Setter Property="Background">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#FF2A3557" Offset="0.004"/>
                        <GradientStop Color="#FFFFFFFF" Offset="1"/>
                        <GradientStop Color="#FF6199CD" Offset="0.388"/>
                        <GradientStop Color="#FF4480A0" Offset="0.737"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="TitleTextBlock" TargetType="TextBlock">
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Foreground" Value="#BBFFFFFF"/>
            <Setter Property="FontFamily" Value="Verdana"/>
        </Style>
        <Style x:Key="SubTitleTextBlock" TargetType="TextBlock">
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="FontFamily" Value="Trebuchet MS"/>
            <Setter Property="Foreground" Value="#BBFFFFFF"/>
        </Style>
        <Style x:Key="TextBlockStyle" TargetType="TextBlock">
            <Setter Property="Margin" Value="3,3,3,3"/>
            <Setter Property="FontFamily" Value="Trebuchet MS"/>
            <Setter Property="TextAlignment" Value="Left"/>
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
        <DataTemplate x:Key="StackedProductTemplate">
            <StackPanel Orientation="Horizontal">
                <StackPanel Style="{StaticResource TitlePanel}"
                    Orientation="Vertical">
                    <TextBlock Text="{Binding ProductName, Mode=OneWay}"
                    Style="{StaticResource TitleTextBlock}"
                    FontSize="14"/>
                <TextBlock Text="{Binding UnitPrice,
                    Mode=OneWay}"
                    Style="{StaticResource SubTitleTextBlock}" />
              </StackPanel>
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition></ColumnDefinition>
                        <ColumnDefinition></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Product ID"
                    Style="{StaticResource TextBlockStyle}"
                    Grid.Row="0" Grid.Column="0"/>
                    <TextBlock Text="{Binding ProductId, Mode=OneWay}"
                    Style="{StaticResource TextBlockStyle}"
                    Foreground="#FF001070" Grid.Row="0" Grid.Column="1"/>
                    <TextBlock Text="Price"
                    Style="{StaticResource TextBlockStyle}" Grid.Row="1"
                    Grid.Column="0"/>
                    <TextBlock Text="{Binding UnitPrice, Mode=OneWay}"
                    Foreground="#FF001070"
                    Style="{StaticResource TextBlockStyle}"
                    Grid.Row="1" Grid.Column="1"/>
                    <TextBlock Text="Units"
                    Style="{StaticResource TextBlockStyle}" Grid.Row="2"
                    Grid.Column="0"/>
                    <TextBlock Text="{Binding UnitsInStock, Mode=OneWay}"
                    Foreground="#FF001070"
                    Style="{StaticResource TextBlockStyle}" Grid.Row="2"
                    Grid.Column="1"/>
               </Grid>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <ListBox x:Name="lstProducts" Height="220" HorizontalAlignment="Left"
        VerticalAlignment="Bottom" Margin="10,10,10,10" Width="480"
        Style="{StaticResource ListBoxStyle}"
        ItemsSource="{Binding}"
        ItemTemplate="{StaticResource StackedProductTemplate}">
        </ListBox>
    </Grid>
</UserControl>

Named templates are easy to set up as resources and to reuse, if needed. If you intend to use a DataTemplate in multiple Silverlight controls, it would be beneficial to put the DataTemplate in the app.xaml file as an application global resource. If the template is designed specifically for a control, it would be better to put the resource in the local file, as shown in Example 4-6. You also could create the resource as a resource of any container control of the ListBox, such as the LayoutRoot Grid. However, it is a better practice to keep resources at the user control level unless there is a specific reason to move the resource closer to where it will be used.

Item Selection

User interfaces often display a list of items to a user in a control such as a ListBox and allow the user to select one of the items. Once he makes the selection, the user may be brought to another screen, another control may appear with detailed information about the selected item, or the selected item’s details may appear in a series of controls below the ListBox. Generally, this is a good design practice, as it is often impossible to effectively display all of the information about an item in a list-based control. For example, if an entity such as a Product has more than 50 properties, displaying all of those properties in a list-based control would be problematic and likely would not be very user-friendly. Though with Silverlight 2 and XAML, you can alleviate this somewhat by creating a DataTemplate that contains several rows of information within each row of the ListBox. However, even this solution has its limits.

One possible solution to this type of problem is to display a list of items that contains a summary of the items in a control such as a ListBox. Then when the user selects an item from the ListBox, the details of all of the properties for the Product are displayed in a series of controls. The code in Example 4-7 shows how to implement this type of scenario.

Example 4-7. Setting the DataContext for the selected product
C#
private void lstProducts_SelectionChanged(object sender,
        SelectionChangedEventArgs e)
{
    Product product = (Product) lstProducts.SelectedItem;
    ProductDetailsLayout.DataContext = product;
}
VB
Private Sub lstProducts_SelectionChanged(ByVal sender As Object, _
        ByVal e As SelectionChangedEventArgs)
    Dim product As Product = CType(lstProducts.SelectedItem, Product)
    ProductDetailsLayout.DataContext = product
End Sub

When a user selects an item from a ListBox, the SelectionChanged event is raised. You can write an event handler for this event that retrieves the selected item and sets the DataContext of a series of detailed controls that are bound to a DataContext that is expecting a product instance. For example, the following code shows the lstProducts_SelectionChanged event handler getting a reference to the lstProducts ListBox’s SelectedItem property. The SelectedItem contains either null or a Product instance. The Product instance is then set to the DataContext of a Grid layout control named ProductDetailsLayout.

Example 4-8 shows the XAML that contains the ListBox and the details controls that will present the Product information. The details controls are contained within the Grid layout control named ProductsDetailsLayout, shown in bold in Example 4-8. When the lstProducts_SelectionChanged event handler executes, it sets the DataContext for this Grid control to the selected product from the lstProducts ListBox. The controls contained within the ProductDetailsLayout Grid control inherit this DataContext, as none of their DataContext properties are set. This allows each of them to bind to the selected item in the ListBox and have the values displayed whenever a user selects a different item.

Example 4-8. XAML for product ListBox and details controls
<Grid x:Name="LayoutRoot" Margin="10,10,10,10">
    <Rectangle Fill="#FFDEE6FB" Grid.Column="0" Grid.Row="0"
        Grid.RowSpan="2" RadiusX="20" RadiusY="20" Opacity="0.8"
        StrokeThickness="0" />
    <StackPanel Grid.Column="0" Grid.Row="0">
        <ListBox x:Name="lstProducts" Height="88" HorizontalAlignment="Left"
            VerticalAlignment="Bottom" Margin="20,20,20,5" Width="440"
            Style="{StaticResource ListBoxStyle}"
            ItemTemplate="{StaticResource ProductTemplate}"
            ItemsSource="{Binding}"/>
        <StackPanel Orientation="Horizontal" x:Name="buttonPanel"
            HorizontalAlignment="Right" Margin="10,0,10,0" >
            <Button x:Name="btnAddProduct" Content="Add Product" Margin="10,5,10,5"
            HorizontalAlignment="Right" Height="30" Width="120"
                Style="{StaticResource ButtonStyle}"/>
            <Button x:Name="btnRemoveProduct" Content="Remove Product"
                Height="30" Width="120" Style="{StaticResource ButtonStyle}"/>
        </StackPanel>
        <Grid Margin="10,10,10,10" x:Name="ProductDetailsLayout">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="123"/>
                <ColumnDefinition Width="Auto" MinWidth="337"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="40"/>
            </Grid.RowDefinitions>
            <TextBlock Text="Product Id" Grid.Row="0" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Product Name" Grid.Row="1" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Qty Per Unit" Grid.Row="2" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Units in Stock" Grid.Row="3" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Units on Order" Grid.Row="4" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Reorder Level" Grid.Column="0"  Grid.Row="5"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Discontinued" Grid.Row="6" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>
            <TextBlock Text="Discontinued Date" Grid.Row="7" Grid.Column="0"
            Margin="10,5,10,5" Style="{StaticResource TextBlockCaptionStyle}"/>

            <TextBlock Text="{Binding Path=ProductId, Mode=OneTime}" Grid.Row="0"
            Grid.Column="1" Margin="11,5,10,5"
            Style="{StaticResource TextBlockReadOnlyStyle}"
            x:Name="lbProductId" HorizontalAlignment="Left"/>
            <TextBox x:Name="tbProductName" Grid.Column="1" Grid.Row="1"
            Margin="10,5,10,5" HorizontalAlignment="Left"
            Height="30"   Width="240"
              Style="{StaticResource TextBoxStyle}"
            Text="{Binding Mode=TwoWay, Path=ProductName}"/>
            <TextBox x:Name="tbQtyPerUnit" Grid.Column="1" Grid.Row="2"
            Margin="10,5,10,5" HorizontalAlignment="Left"
            Height="30" Width="240"
              Style="{StaticResource TextBoxStyle}"
            Text="{Binding Mode=TwoWay, Path=QuantityPerUnit}"/>
            <StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal">
                <Slider HorizontalAlignment="Left" Margin="10,5,10,5"
                x:Name="sliUnitsInStock" VerticalAlignment="Stretch"
                LargeChange="10" Maximum="150" SmallChange="1" Width="240"
                   Value="{Binding Mode=TwoWay, Path=UnitsInStock}"
                Style="{StaticResource SliderStyle}" />
                <TextBlock Text="{Binding Path=UnitsInStock, Mode=OneWay}"
                Margin="5,5,10,5"
                Style="{StaticResource TextBlockReadOnlyStyle}"
                x:Name="lbUnitsInStock" HorizontalAlignment="Left"/>
            </StackPanel>
            <StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal">
                <Slider HorizontalAlignment="Left" Margin="10,5,10,5"
                x:Name="sliUnitsOnOrder" VerticalAlignment="Stretch"
                LargeChange="10" Maximum="150" SmallChange="1" Width="240"
                Value="{Binding Mode=TwoWay, Path=UnitsOnOrder}"
                Style="{StaticResource SliderStyle}" />
                <TextBlock Text="{Binding Path=UnitsOnOrder, Mode=OneWay}"
                Margin="5,5,10,5"
                Style="{StaticResource TextBlockReadOnlyStyle}"
                x:Name="lbUnitsOnOrder" HorizontalAlignment="Left"/>
            </StackPanel>
            <StackPanel Grid.Row="5" Grid.Column="1" Orientation="Horizontal">
                <Slider Margin="10,5,10,5" HorizontalAlignment="Left"
                Style="{StaticResource SliderStyle}"
                Value="{Binding Mode=TwoWay, Path=ReorderLevel}"
                VerticalAlignment="Stretch" x:Name="sliReorderLevel"
                LargeChange="10" Maximum="150" SmallChange="1" Width="240" />
                <TextBlock Text="{Binding Path=ReorderLevel, Mode=OneWay}"
                Margin="5,5,10,5"
                Style="{StaticResource TextBlockReadOnlyStyle}"
                x:Name="lbReorderLevel" HorizontalAlignment="Left"/>
            </StackPanel>
            <CheckBox x:Name="cbDiscontinued" Grid.Column="1" Grid.Row="6"
            Margin="10,5,10,5" HorizontalAlignment="Left" Height="30" Width="240"
              Style="{StaticResource CheckBoxStyle}"
            IsChecked="{Binding Mode=TwoWay, Path=Discontinued}"/>
            <TextBlock x:Name="lbDiscontinuedDate" Grid.Column="1" Grid.Row="7"
            Margin="10,5,10,5" HorizontalAlignment="Left" Height="13" Width="240"
              Style="{StaticResource TextBlockReadOnlyStyle}"
            Text="{Binding Mode=OneWay, Path=DiscontinuedDate}"
            VerticalAlignment="Center" />
        </Grid>
        <StackPanel Orientation="Horizontal" x:Name="bottomButtonPanel"
            Height="40" HorizontalAlignment="Right" Margin="10,0,10,0" >
            <Button x:Name="btnOK" Content="OK" Height="30" Width="120"
            Style="{StaticResource ButtonStyle}"/>
        </StackPanel>
    </StackPanel>
</Grid>

Using Different Binding Modes

Each details control in Example 4-8 uses a different binding mode. The ProductId property is the identifier for the Product entity and it will not be changed. This property value is displayed in a TextBlock, because it is read-only and the binding mode is set to OneTime. OneTime tells the target property that it need not listen for change notifications on this property, as the property will never change. Therefore, the only time this TextBlock’s value will be updated is when the DataContext is set.

The ProductName property is bound to a TextBox target control, because the user may decide to change this value. The binding mode is set to TwoWay for this and many other controls. TwoWay allows the user to change the value in the target control and have the new value sent automatically to the bound data source. It also tells the target control to listen for property change notifications so that it can update the contents of the TextBox if this event is raised for its bound property.

The UnitsInStock, ReorderLevel, and UnitsOnOrder properties are bound to two different controls. Each property is bound to a Slider and to a TextBlock control. The Slider is bound using the TwoWay binding mode to the corresponding property so that when the user moves the Slider left or right, the value is sent automatically back to the bound data source. The Slider control does not display a value, however, so a TextBlock has been placed to the right of each Slider control to display the Slider’s value. In Example 4-8, this is implemented entirely through declarative code using XAML. The TextBlock’s Text property and the Slider’s Value property are both bound to the same property of the Product data source. The Slider’s binding uses the TwoWay binding mode and the TextBlock uses the OneWay binding mode. The TextBlock cannot be updated, so it need not use the TwoWay binding mode. The TextBlock must listen for PropertyChanged events so that it knows when to display the updated value.

For example, when the user slides the Slider named sliReorderLevel (shown in bold in the middle of Example 4-8) to the right, increasing the value to 50, that value is sent to the Product instance, which calls the setter code for the ReorderLevel property, which raises the PropertyChanged event, which notifies anyone listening (in this case, the TextBlock named lbReorderLevel) and updates the target control’s value. The bindings shown in Example 4-8 demonstrate that all three types of binding modes can be valuable assets when used in the same Silverlight control. The presentation of the XAML in Example 4-8 appears in Figure 4-4. (Refer to Figure 4-5 in the next section for a look at the updated reorder level.)

ProductView Silverlight control
Figure 4-4. ProductView Silverlight control

Lists and Notifications

Entities can implement the INotifyPropertyChanged interface to communicate with the target controls to notify them when a property value has changed. Lists of items can also receive notifications when the contents of the list have changed. The ObservableCollection<T> is a special collection that works well with XAML-based bindings to notify binding targets when a change has been made to a list of items in the ObservableCollection<T>.

ObservableCollection<T>

The ObservableCollection<T>, part of the System.Collections.ObjectModel namespace, implements both the INotifyPropertyChanged and INotifyCollectionChanged interfaces. When a class inherits from the ObservableCollection<T>, the derived class also gets the benefit of the INotifyPropertyChanged interface. This allows the derived class to raise the PropertyChanged event for the ObservableCollection<T>-derived class when a property on the collection has changed.

Note

Although this section of the chapter focuses on using the ObservableCollection<T> for list-based binding and notifications, any collection class that implements the INotifyCollectionChanged interface will also reap the benefits discussed here.

Notice that in Figure 4-5, where the members of ObservableCollection<T> are displayed, the PropertyChanged event is listed as a protected event, which allows classes that inherit from ObservableCollection<T> to implement the event. In fact, most of the members of the ObservableCollection<T> collection are protected, as they exist to help a derived class implement and manage the list of items using the ObservableCollection<T>. The only two public members are the constructor and the CollectionChanged event.

ObservableCollection<T> members
Figure 4-5. ObservableCollection<T> members

The ObservableCollection<T> also implements the INotifyCollectionChanged interface. This interface contains a single member which is the CollectionChanged event. The ObservableCollection<T> raises this event automatically whenever an item in the ObservableCollection<T> is added or removed from the list. Binding targets in Silverlight receive these notifications and allow list-based controls to update themselves upon receiving a change notification from one of these interfaces when one of their change events fires.

Changing a List<T>

You can bind list-based controls directly to an object source or a list of objects, such as a List<T> or an ObservableCollection<T>. This is especially useful for binding a list of objects to a list-based control, such as the ListBox or DataGrid, or a third-party list-based control, when these controls must be updated when their bound lists change. The ObservableCollection<T> exists to help fill in the gaps of notifying binding targets where a regular list-based object does not notify the binding targets of changes.

For example, Figure 4-6 shows a list of products contained within a List<T>. This List<Product> is set to the DataContext for the ListBox’s container control (in this case, the grid layout control). The ListBox is loaded with a List<Product> containing four Product objects. Example 4-9 shows the four Product objects created in the ProductView.xaml.cs code file. When items are added to this List<Product>, no notifications are sent and the data-bound targets never update the ListBox contents.

ProductView showing updated reorder level
Figure 4-6. ProductView showing updated reorder level
Example 4-9. Creating a list of product entities
C#
private List<Product> CreateProductList()
{
    List<Product> products = new List<Product>
        {
            new Product
                {
                    ProductId = 70, ProductName = "Outback Lager",
                    QuantityPerUnit = "24 - 355 ml bottles",
                    UnitsInStock = 15,  UnitPrice = 15,
                    UnitsOnOrder = 10,  ReorderLevel = 30,
                    Discontinued = false
                },
            new Product
                {
                    ProductId = 71, ProductName = "Flotemysost",
                    QuantityPerUnit = "10 - 500 g pkgs.",
                    UnitsInStock = 25, UnitPrice = (decimal)21.5,
                    UnitsOnOrder = 0, ReorderLevel = 0,
                    Discontinued = false
                },
            new Product
                {
                    ProductId = 35, ProductName = "Steeleye Stout",
                    QuantityPerUnit = "24 - 12 oz bottles",
                    UnitsInStock = 20, UnitPrice = (decimal)18,
                    UnitsOnOrder = 0, ReorderLevel = 15,
                    Discontinued = false
                },
            new Product
                {
                    ProductId = 53, ProductName = "Perth Pasties",
                    QuantityPerUnit = "48 pieces",
                    UnitsInStock = 0, UnitPrice = (decimal)32.80,
                    UnitsOnOrder = 0, ReorderLevel = 0,
                    Discontinued = true,
                    DiscontinuedDate = new DateTime(1996, 7, 4)
                }
        };
    return products;
}
VB
Private Function CreateProductList() As List(Of Product)
    Dim products As List(Of Product) = New List(Of Product) _
    (New Product() {New Product With _
        {.ProductId = 70, .ProductName = "Outback Lager", _
        .QuantityPerUnit = "24 - 355 ml bottles", .UnitsInStock = 15, _
        .UnitPrice = 15, .UnitsOnOrder = 10, .ReorderLevel = 30, _
        .Discontinued = False}, _
    New Product With {.ProductId = 71, .ProductName = "Flotemysost", _
        .QuantityPerUnit = "10 - 500 g pkgs.", .UnitsInStock = 25, _
        .UnitPrice = CDec(21.5), .UnitsOnOrder = 0, .ReorderLevel = 0, _
        .Discontinued = False},
    New Product With {.ProductId = 35, .ProductName = "Steeleye Stout", _
        .QuantityPerUnit = "24 - 12 oz bottles", .UnitsInStock = 20, _
        .UnitPrice = CDec(18), .UnitsOnOrder = 0, .ReorderLevel = 15, _
        .Discontinued = False}, _
    New Product With {.ProductId = 53, .ProductName = "Perth Pasties", _
        .QuantityPerUnit = "48 pieces", .UnitsInStock = 0, _
        .UnitPrice = CDec(32.80), .UnitsOnOrder = 0, .ReorderLevel = 0, _
        .Discontinued = True, .DiscontinuedDate = New DateTime(1996, 7, 4)} _
    })
    Return products
End Function

When a user clicks the Add Product button, a new Product instance is created and is added to the List<Product> that is bound to the DataContext. When this new Product instance is created the List<Product> expands to contain it; however, the ListBox whose ItemsSource is bound to the same DataContext does not add the new Product to its items collection. As far as the user can tell, the Add Product button did not add a new Product instance at all. The code for adding a new Product instance simply creates a fake new Product with some dummy values, as shown in Example 4-10.

When the Product is added to the productList, which is set to the DataContext of the ListBox, the ListBox is not made aware of the newly added item. The ListBox is fully capable of adding the new item to its contents without refreshing the list. However, for the ListBox to add the new item to its list of items, the data source list must raise the CollectionChanged event. The CollectionChanged event is a member of the INotifyCollectionChanged interface, which the ObservableCollection<T> implements inherently. The List<T> does not implement any notifications on its own.

Note

The complete code for List<T> is in ProductView.xaml.cs in the sample code for this chapter.

Example 4-10. Adding a product to the list
C#
private void btnAddProduct_Click(object sender, RoutedEventArgs e)
{
    Product product = new Product { Discontinued = false,
        DiscontinuedDate = new DateTime(2008, 7, 11), ProductId = 1111,
        ProductName = "Test Product", QuantityPerUnit = "1",
        ReorderLevel = 10, UnitPrice = 5, UnitsInStock = 20,
        UnitsOnOrder = 0 };
    productList.Add(product);
}
VB
Private Sub btnAddProduct_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
    Dim product As Product = New Product With {.Discontinued = False, _
        .DiscontinuedDate = New DateTime(2008, 7, 11), _
        .ProductId = 1111, .ProductName = "Test Product", _
        .QuantityPerUnit = "1", .ReorderLevel = 10, .UnitPrice = 5, _
        .UnitsInStock = 20, .UnitsOnOrder = 0}
    productList.Add(product)
End Sub

When a user selects an item from the ListBox and clicks the Remove Product button, the item is indeed removed from the data source List<Product> that is set to the DataContext. However, once again the List<Product> does not raise the CollectionChanged event, so the ListBox does not remove the selected item from its items collection. The ListBox’s ItemsSource property is the target of a binding operation. The bound item has been changed (an item was removed from the List<Product>). However, the data source in this case does not raise the CollectionChanged event, so the ListBox is never told that one of its items should be removed.

Changing an ObservableCollection<T>

You can modify this example by using an ObservableCollection<T> instead of a List<T> to contain the set of products. You could modify the code in Example 4-9 slightly to create an instance of an ObservableCollection<Product> and to return that instance from the CreateProductList method. The code in Example 4-11 shows the first few lines of the newly revised CreateProductList method to handle the ObservableCollection<Product>.

Example 4-11. Creating an ObservableCollection
C#
private ObservableCollection<Product> CreateProductOC()
{
    ObservableCollection<Product> products = new ObservableCollection<Product>
VB
Private Function CreateProductOC() As ObservableCollection(Of Product)
    Dim products As ObservableCollection(Of Product) = _
        New ObservableCollection(Of Product)

The ObservableCollection<T> implements the INotifyCollectionChanged interface, which automatically raises the CollectionChanged event when an item is added or removed from the ObservableCollection<T>’s internal collection. List-based data-bound targets listen for the event. When you change the code for this example to use an ObservableCollection<Product> instead of a List<Product> and the user adds or removes an item from the ListBox, the ListBox listens for the event and adds or removes the item as directed. For example, if the user clicks on the Add Product button several times, the ObservableCollection<Product> will add several new Product instances to the collection, each raising the CollectionChanged event, which causes the ListBox to add the new items to its items collection. To the user, it simply looks like the Add Product button works.

When the user selects an item from the ListBox and clicks the Remove Product button, the code in Example 4-12 is executed and the item is removed from the ObservableCollection<Product>. The ListBox listens for the CollectionChanged event once again and updates the ListBox’s items collection. To the user, the ListBox simply removed the selected item. As you can see, it is very easy to implement the ObservableCollection<T> and take advantage of its notifications.

Note

You can find the complete code for ObservableCollection<T> in ProductViewOC.xaml.cs in the sample code for this chapter.

Example 4-12. Removing a product
C#
private void btnRemoveProduct_Click(object sender, RoutedEventArgs e)
{
    Product product = lstProducts.SelectedItem as Product;
    if (product == null)
        return;

    if (productList.Contains(product))
        productList.Remove(product);
}
VB
Private Sub btnRemoveProduct_Click(ByVal sender As Object, _
        ByVal e As RoutedEventArgs)
    Dim product As Product = TryCast(lstProducts.SelectedItem, Product)
    If product Is Nothing Then
        Return
    End If

    If productList.Contains(product) Then
        productList.Remove(product)
    End If
End Sub

Converters

When binding an entity object source to Silverlight 2 controls, it is often desirable to present the values in the target in a different format from the source. For example, when a date value is stored in a database, you almost always must format the value when you’re displaying it in a user interface. You might need to convert this value to an MM/DD/YYYY format to be displayed in a Silverlight user control. Another example might be a string property of an entity that represents a color, such as blue. You might intend to use this value as a Brush for a Silverlight 2 control. You must convert these types of values from their native format into a format that is appropriate. Type conversions are common and you can tackle them by creating a class that implements the IValueConverter interface.

IValueConverter

The IValueConverter interface exposes two methods that must be implemented. The Convert and ConvertBack methods handle converting a value to and from the target and source of a binding. The Convert method morphs the data on its way from the source to the target. The ConvertBack method morphs the data from the target and back to the source.

Using a converter requires the following steps. First, you must create a converter class that implements the IValueConverter interface. Next, you must implement both the Convert and ConvertBack methods. The Convert method converts the value from the source to the target. This conversion is required for all binding modes. The ConvertBack method is called in TwoWay binding when the value in the target is sent back to the source. If the binding mode is OneWay or OneTime, the value is never sent from the target back to the source, so the ConvertBack method is not used.

Once you have created the converter class and implemented the IValueConverter methods, you can apply the converter in an appropriate binding scenario. You must declare an instance of the converter as a resource in the XAML page in order to do this. Then you can set the Converter property in the binding syntax to use the converter class instance. For example, the following code snippet declares a converter class instance as a UserControl resource:

    <UserControl.Resources>
        <ListBindings:DateTimeConverter x:Key="myDateTimeConverter" />
    </UserControl.Resources>

The following applies this converter in a binding operation:

Text="{Binding Mode=OneWay, Path=DiscontinuedDate,
        Converter={StaticResource myDateTimeConverter}}"

Conversions

Figure 4-6 shows the ProductView Silverlight control with its Discontinued Date using the default DateTime format. You could convert this value to use a different format to display in the Silverlight control if it applies a class that implements the IValueConverter interface. The Product class also has a UnitPrice property, which is not displayed on the Silverlight control. The UnitPrice is stored as a decimal value, but it should be displayed as a currency value with exactly two decimal places. Once you have added the UnitPrice to the Silverlight control, along with the conversions, the data will be formatted properly for the user. Example 4-13, which appears in the following section, will build on the ProductView control by adding the UnitPrice property and using the converters for the DateTime and the Decimal values. You can find the code for this control, named ProductViewWithConverter.xaml, in the code for this chapter.

DateTime conversions

Example 4-13 shows the DateTimeConverter class that converts a DateTime value to a string value using a specific date format of MM/DD/YYYY. The Convert method is implemented and grabs the value parameter which represents the DateTime value that is being sent from the source to the target. The Convert method intercepts the DateTime value and converts it to a formatted string. The ConvertBack method is not implemented, because the date value will not be involved in a TwoWay binding operation in this example.

You can use the targetType parameter to make sure the binding is operating on the appropriate target type. For example, in this case the target should be a property that is looking for a string value. The targetType is especially valuable when converting a value to a specific property type, such as a Brush or an enumeration.

Example 4-13. DateTimeConverter
C#
public class DateTimeConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        DateTime theDate = (DateTime)value;
        string formattedDate = string.Empty;

        if (parameter == null)
            formattedDate = theDate.ToString(culture);
        else
            formattedDate = theDate.ToString(parameter as string, culture);

        return formattedDate;
    }

    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        throw new System.NotImplementedException();
    }
}
VB
Public Class DateTimeConverter
    Implements IValueConverter
    Public Function Convert(ByVal value As Object, ByVal targetType As Type, _
            ByVal parameter As Object, ByVal culture As CultureInfo) As Object
        Dim theDate As DateTime = CDate(value)
        Dim formattedDate As String = String.Empty

        If parameter Is Nothing Then
            formattedDate = theDate.ToString(culture)
        Else
            formattedDate = theDate.ToString(TryCast(parameter, String), culture)
        End If

        Return formattedDate
    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, _
            ByVal parameter As Object, ByVal culture As CultureInfo) As Object
        Throw New System.NotImplementedException()
    End Function
End Class

The following line adds an instance of the converter to the UserControl’s resource section:

<ListBindings:DateTimeConverter x:Key="myDateTimeConverter" />

The XAML for the lbDiscontinuedDate TextBlock is modified to include the converter in the binding operation. The resultant XAML for this TextBlock will look like the XAML snippet in Example 4-14.

Example 4-14. Using a converter in XAML
<TextBlock x:Name="lbDiscontinuedDate" Grid.Column="1" Grid.Row="8"
    Margin="10,5,10,5" HorizontalAlignment="Left" Height="13" Width="240"
    Style="{StaticResource TextBlockReadOnlyStyle}"
    Text="{Binding Mode=OneWay, Path=DiscontinuedDate,
        Converter={StaticResource myDateTimeConverter}}"
        VerticalAlignment="Center" />

Currency conversions

The Grid panel in Example 4-13 is adjusted to contain the new tbUnitPrice TextBox. The value must be converted from a decimal to a guaranteed two-decimal value string. The Convert method intercepts the decimal value on its way from the source to the target TextBox’s Text property. Because this TextBox is involved in a TwoWay binding operation, the ConvertBack method intercepts the value on its way back from the target to the source. The XAML snippet in Example 4-15 shows the tbUnitPrice TextBox code.

Example 4-15. Setting a ConverterParameter
<TextBox x:Name="tbUnitPrice" Grid.Column="1" Grid.Row="2"
    Margin="10,5,10,5" HorizontalAlignment="Left" Height="30" Width="240"
    Style="{StaticResource TextBoxStyle}"
    Text="{Binding Mode=TwoWay, Path=UnitPrice,
        Converter={StaticResource myCurrencyConverter}},
        ConverterParameter=C}"/>

Notice that a parameter is passed to the converter through the ConverterParameter property in the XAML. The ConverterParameter property allows XAML to influence how the converter class handles the conversion. In this case, the currency format is passed to the converter.

The following XAML snippet creates the instance of the CurrencyConverter class in the resource section of the UserControl:

<ListBindings:CurrencyConverter x:Key="myCurrencyConverter" />

The CurrencyConverter class code in Example 4-16 shows the simple conversion from the decimal value to the formatted string value in the Convert method. Once these changes are made, the ProductViewWithConverter control will display the values, as shown in Figure 4-7.

ProductViewWithConverter—using converters
Figure 4-7. ProductViewWithConverter—using converters
Example 4-16. CurrencyConverter
C#
public class CurrencyConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        decimal amount = System.Convert.ToDecimal(value);
        string c = amount.ToString(parameter as string, culture);
        return c;
    }

    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        throw new System.NotImplementedException();
    }
}
VB
Public Class CurrencyConverter
    Implements IValueConverter
    Public Function Convert(ByVal value As Object, ByVal targetType As Type, _
            ByVal parameter As Object, ByVal culture As CultureInfo) As Object
        Dim amount As Decimal = System.Convert.ToDecimal(value)
        Dim c As String = amount.ToString(TryCast(parameter, String), culture)
        Return c
    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, _
        ByVal parameter As Object, ByVal culture As CultureInfo) As Object
        Throw New System.NotImplementedException()
    End Function
End Class

This example shows how you can use converters to intercept and morph values on their way from a source to a target and vice versa. You can use converters to convert from any type of source value to any type of target value, as long as the conversion follows common-sense rules. For example, it would make sense in some cases to convert an enumeration value that represents a color to a Brush. However, it would not make sense to convert a DateTime value to a slider control’s value property. If you use converters appropriately, though, you can use them throughout an application to achieve consistent data conversion to and from bindings.

Summary

Two of the key sources of the power of XAML-based data binding are the INotifyPropertyChanged interface and the ObservableCollection<T>. These classes provide the means for data source objects to notify data bindings that something about the source has changed. Converters are integral components of the binding process, as they can seamlessly convert values involved in both one-way and two-way binding operations.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required