Chapter 16. Visualizing Lists

What You’ll Learn in This Hour:

  • List controls and their relationships

  • ItemsControl

  • Customizing lists

  • Finishing the Contact Manager

In many of the previous hours, we have made use of various controls for displaying lists. We have touched them only on a surface level, frequently mentioning that we would cover them in detail at a later time. In this hour we finally dig into this powerful portion of the WPF control hierarchy and explore many of the great possibilities that it enables.

The Control Hierarchy

Take a few minutes to study Figures 16.1 and 16.2. If you look closely, you’ll see almost every control we have discussed thus far in the book represented somewhere in one of these two charts. There are a few notable exceptions, but almost every control we have used either inherits from ItemsControl or ContentControl. We know that ContentControl has a Content property that allows it to render almost anything, and we’ve seen references to ItemsControl throughout the book as it relates to lists. We could state that the primary difference between these two base classes is that ContentControl supports the rendering of a single item, whereas ItemsControl supports the rendering of multiple items.

The most common list-related classes.

Figure 16.1. The most common list-related classes.

The most common content-related classes.

Figure 16.2. The most common content-related classes.

For the remainder of this hour, we look at several important features of all ItemsControl derivatives. We’ll also pay special attention to classes that inherit from Selector. As we uncover important aspects of these base classes, we’ll extend the Contact Manager with some new features.

Dissecting ItemsControl

Looking back over Figure 16.1 we can see just how many controls there are that are related to ItemsControl. The good part about this is that everything we discuss here regarding the base class can be applied to the usage of every derivative.

Items

The first order of business with an ItemsControl is being able to tell it what items should be displayed. This can be accomplished in two ways. The simplest and most intuitive way is to add items to its Items collection by calling the Add method and passing in the item. This can also be done in XAML by declaring multiple child elements. To see an example of where we have used this technique, open Shell.xaml in the Contact Manager. Near the top of the markup you see the definition of a StatusBar. We know from Figure 16.1 that StatusBar is an ItemsControl. In this case, its Items collection is being explicitly populated by three instances of StatusBarItem. In many cases you will want your lists to be data driven. For this second scenario, ItemsControl exposes an ItemsSource property. You can set any IEnumerable collection as the value and the ItemsControl will use this to generate the Items collection. If you open the SideBar.xaml, you will see an example of where we have used data binding to set the ItemsSource collection.

By the Way

The performance of ItemsControl and some advanced data binding scenarios are influenced by what type of collection you use for the ItemsSource. In general, ObservableCollection is the best in terms of performance and features, followed by List, and ending with plain old IEnumerable.

Rendering

Setting up the items for an ItemsControl is pretty straightforward, but understanding how those items get rendered can be a little confusing. In the case of the above-mentioned StatusBar, it’s fairly obvious that it renders the StatusBarItem instances. But what about the ItemsControl in SideBar.xaml? It’s bound to a list of Contact instances. How does WPF determine how to render these? Following is a list of the steps that ItemsControl goes through to render the instances in its Items collection:

  1. If the item inherits from Visual, WPF already knows how to render this type and will treat it normally.

  2. If the item does not inherit from Visual (like Contact), the ItemsControl will look for special instructions as detailed in the following:

    • If the DisplayMemberPath property is set, the ItemsControl creates a TextBlock and binds its Text property to the property of the item indicated by DisplayMemberPath.

    • If the ItemTemplate is set to a DataTemplate, the ItemsControl uses the template to render each item.

    • If the ItemTemplateSelector is set, WPF executes your custom code (a special class inheriting from DataTemplateSelector), expecting you to provide it with a DataTemplate based on the particular item in the collection.

    • If none of the preceding properties is set, the ItemsControl searches the Resources hierarchy looking for a DataTemplate that matches the item type. If none is found, the ToString method on the item is called and a TextBlock is used to display this value.

  3. After the visuals are created for the display of the item, the ItemsControl uses its ItemContainerGenerator to wrap that visual in an appropriate container. For example, if you were using a ComboBox, it would wrap the visual in a ComboBoxItem.

  4. The generated container would then be styled according to the following rules:

    • If the ItemContainerStyle is set, the ItemsControl will use it to style each container.

    • If the ItemContainerStyleSelector was set, it would execute your custom code (a special class inheriting from StyleSelector), expecting you to provide it with a Style based on the particular item in the collection.

    • If none of the preceding properties is set, the ItemsControl searches the Resources hierarchy looking for a Style that matches the container type. If one is not found, the default style will be applied.

ItemContainerGenerator

As alluded to previously, every ItemsControl has an ItemContainerGenerator. This class is responsible for generating appropriate containers for the data-bound UI. In the case of a ListBox, its generator would create instances of ListBoxItem, but in the case of ComboBox, the generator would produce instances of ComboBoxItem. Normally, a developer doesn’t need to interact with this directly. However, occasionally there is a need to determine what container instance is hosting a particular piece of data or vice versa. To do this, ItemContainerGenerator has several methods. We have found the most useful methods to be ItemFromContainer and ContainerFromItem.

DataTemplateSelector

Sometimes a user interface’s portrayal of data needs to be based on complex logic and can only be determined at runtime. In these cases, rather than setting the ItemTemplate for an ItemsControl, you want to use a DataTemplateSelector. To implement your special display logic, you must inherit from this class and override its SelectTemplate method. The code should look something like this:

public class MyTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        DataTemplate dataTemplate;

        //custom logic for determining the template

        return dataTemplate;
    }
}

The custom selector is generally instantiated in a Resources collection so that it can be referenced elsewhere and so that it can reference other templates stored in the resources also. At runtime, the item parameter of the SelectTemplate method refers to the data that is being displayed by the template, and the container parameter refers to the UI container that is hosting the data (such as a ListBoxItem). Typically, your custom selector implementation exposes properties for the different templates it needs so that they could be bound to other resources. The SelectTemplate method then programmatically determines which of these templates to use based on properties of the data item. By tackling the problem this way, you can still define your templates in XAML (bound using StaticResource), but have your custom selection logic in code.

By the Way

There is another way to alter the appearance of templates at runtime without resorting to custom code: Triggers. Triggers are discussed in Hour 22.

StyleSelector

StyleSelector follows the same pattern as DataTemplateSelector, except that it has a SelectStyle method. The parameters are the same as SelectTemplate in the previous example, and the techniques used are the same as well.

As you can see, the ItemsControl is very flexible and can receive its item rendering instructions in a variety of ways. The most common scenarios are to either set DisplayMemberPath or to use a DataTemplate. In the case of SideBar.xaml, the default ToString is used. We’ll change this shortly.

Layout

The flexibility of ItemsControl is not limited to the rendering of its individual items, but also allows for the customization of their layout. To customize the layout, you set the ItemsPanel property to an instance of ItemsPanelTemplate. This template describes what type of Panel should be used to lay out the items. By default a VirtualizingStackPanel is used by most descendants of ItemsControl. This is a special type of StackPanel that is smart enough to not attempt the rendering of nonvisible elements. You could tell the ItemsControl to use a Grid, a Canvas, or any other Panel, though.

Another way in which the layout of an ItemsControl can be customized is by setting the GroupStyle (or the GroupStyleSelector) property. Using a CollectionViewSource, you can specify how items should be grouped. The ItemsControl understands these groups and will attempt to render them according to the GroupStyle.

An immense number of features are packed into this one control. Remember that everything that inherits from ItemsControl (everything in Figure 16.1) has these capabilities. Let’s apply some of these features specifically in our Contact Manager.

Customizing the SideBar

We are now going to work with the SideBar.xaml. Currently it displays a very simple, barely usable list of contacts. We’ll spice it up using some of our newfound knowledge. Let’s get started working on that now:

  1. Open the Contact Manager project in Visual Studio if it is not already open.

  2. Open the SideBar.xaml file.

  3. Replace the current ItemsControl markup with what is found in Listing 16.1.

  4. Run the application. Create a few contacts and observe the appearance of the sidebar. You should see something similar to Figure 16.3.

    Example 16.1. An ItemsControl with ItemTemplate

    <ItemsControl Width="250"
                  VerticalAlignment="Stretch"
                  BorderThickness="0"
                  ItemsSource="{Binding CurrentContacts}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Grid Margin="2">
                    <Border Margin="2 2 0 0"
                            CornerRadius="4"
                            Background="Gray"
                            Opacity=".5" />
                    <Border BorderBrush="{StaticResource redBrush}"
                            BorderThickness="2"
                            CornerRadius="4"
                            Background="White"
                            Margin="0 0 2 2"
                            Padding="3">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition />
                                <RowDefinition />
                            </Grid.RowDefinitions>
    
                            <TextBlock Grid.ColumnSpan="2"
                                       FontWeight="Bold"
                                       Text="{Binding LookupName}" />
    
                                <TextBlock Grid.Row="1"
                                           Text="   Office: " />
                                <TextBlock Grid.Row="1"
                                           Grid.Column="1"
                                           Text="{Binding Path=OfficePhone,  Converter={StaticResource  phoneConverter}}"/>
                                <TextBlock Grid.Row="2"
                                           Text="   Email: " />
                                <TextBlock Grid.Row="2"
                                           Grid.Column="1"
                                           Text="{Binding PrimaryEmail}" />
                            </Grid>
                    </Border>
                    <Button Style="{StaticResource openButton}" />
                </Grid>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

    A templated contact list.

    Figure 16.3. A templated contact list.

It may not have occurred to you when we first introduced data templates in Hour 15, “Digging Deeper into Data Binding,” but you can use any type of element to create your template. In this case we’ve simulated a “contact card” style. Notice that from within a DataTemplate you can make full use of styles and converters (and even other templates). You can even catch events:

  1. Add the following attached event declaration to the ItemsControl:

    ButtonBase.Click="OpenContact_Click"
  2. In the SideBar.xml.cs add the following event handler:

    private void OpenContact_Click(object sender, RoutedEventArgs e)
    {
        Button button = e.OriginalSource as Button;
    
        if (button != null)
            Presenter.OpenContact(button.DataContext as Contact);
    }
  3. Don’t forget to add the using statement to the top of the file:

    using ContactManager.Model;
  4. Run the application. You should now be able to use the plus (+) buttons on each of the contact cards to open the contact.

As you can see, DataTemplates used as an ItemTemplate behave naturally, bubbling and tunneling events the same as anything else. We’re almost finished with the sidebar now. Let’s add some sorting to finish things off:

  1. At the top of the UserControl add the following resources definition:

    <UserControl.Resources>
        <CollectionViewSource x:Key="contactSource"
                              Source="{Binding CurrentContacts}">
            <CollectionViewSource.SortDescriptions>
                <cm:SortDescription PropertyName="LookupName" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </UserControl.Resources>
  2. For the SortDescription to work, you need to add this Xml Namespace Declaration:

    xmlns:cm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
  3. Change the ItemsControl.ItemsSource to use our new CollectionViewSource with this binding:

    ItemsSource="{Binding Source={StaticResource contactSource}}"
  4. Run the application, and you should have a fully interactive and sorted contact list.

As you can see, ItemsControl is quite powerful. We could not demonstrate every feature in this example, but we wanted to make you aware of the possibilities. We’ll use additional features such as ItemsPanel and GroupStyle in Hour 17, “Building a Media Viewer,” so you’ll be ready for it when you see it.

Studying Selector

If we look again at Figure 16.1 we see that there is another important base class descended directly from ItemsControl. The Selector class is probably the next most important to understand because its functionality is shared by several very commonly used controls: ComboBox, ListBox, ListView, and TabControl. Selector takes all the functionality of ItemsControl and adds to it the capability to select one or more items. Several important properties come into play at this level. We would like to discuss those properties which we have found the most useful.

To begin with, you can determine which item is selected (or set the selected item) using the SelectedItem property. You can achieve the same effect by working with the SelectedIndex property. This second property represents which element index in the Items collection is selected. Anytime the selection changes, the SelectionChanged event is fired. You can handle this directly or using attached events, as we did with the ComboBox in the Text Editor toolbar from Hour 9. One final property to note is IsSynchronizedWithCurrentItem, which allows you to synchronize the selection across multiple selectors. For example, you might have two ComboBox instances bound to the same collection, and they need to keep their SelectedItem synchronized.

Using Selector

We’ve been using controls that inherit from Selector since the very beginning. But let’s take one more opportunity to examine some XAML and consider it based on our new understanding:

  1. Open the EditContactView.xaml file and locate the ComboBox element. You may want to use the Visual Studio shortcut, Ctrl+F, to more quickly find it.

  2. Notice that the SelectedItem property, which it inherits from Selector, is data bound to our model.

  3. ComboBox also has an ItemsSource property it inherits from ItemsControl that is not yet set, preventing the control from actually displaying any states. To fix this, open App.xaml.

  4. We are going to declare a global data source for our state list. Add the following markup to the application Resources collection, inside the ResourceDictionary:

    <ObjectDataProvider x:Key="stateNames"
                        MethodName="GetNames"
                        ObjectType="{x:Type Model:States}" />
  5. You will need to add the following namespace declaration to the XAML as well:

    xmlns:Model="clr-namespace:ContactManager.Model"
  6. Return to the EditContactView.xaml and go to the ComboBox. Add the following attribute to the ComboBox:

    ItemsSource="{Binding Source={StaticResource stateNames}}"
  7. Run the application. You should now be able to select states in the ComboBox.

  8. Exit the application and return to Visual Studio.

  9. Add the following attribute to the ComboBox:

    IsSynchronizedWithCurrentItem="True"
  10. Run the application again. Open several tabs with contacts in them. Change the selected State in one of the combo boxes.

  11. Go to another tab. Notice that the State change in one tab has affected the other.

  12. Exit the application and return to Visual Studio.

  13. Remove the IsSynchronizedWithCurrentItem attribute because this is not the behavior that we want in this situation.

Hopefully this last task helped you to see how each of the base classes contributed functionality to ComboBox. Also, it is important to understand just how the IsSynchronizedWithCurrentItem property works, because using this in some scenarios can cause surprising and undesirable behavior.

To make the States available to our ComboBox, we’ve used a new technique. The ObjectDataProvider is a simple class that lets us point to an ObjectType and a Method. At runtime, the provider locates the ObjectType and calls the method to obtain a set of data. By placing this in the application resources, we have made available a single state list that the entire application can use.

Summary

In this hour we have taken a deep dive into the world of ItemsControl. As one of the most important base classes in WPF, this control provides a great deal of functionality that will affect much of what you build with this technology. This control allows for the rendering of multiple items, allowing for custom templating and stylization of each item in the list. Additionally, ItemsControl can arrange its items using any type of Panel and can perform grouping of items as well. For selectable lists, WPF offers Selector and its descendents, each one meeting a specific UI need and providing a wealth of UI possibilities.

Q&A

Q.

What are some of the other important base classes besides ContentControl and ItemsControl?

A.

A couple of other base classes that you should be familiar with are RangeBase and TextBoxBase.

Q.

I can control the layout of items using the ItemsPanel property, but if I am using grouping, can I control how the groups are arranged?

A.

Yes. GroupStyle has a number of useful properties. Among them is a Panel property.

Workshop

Quiz

1.

Name four ways that a DataTemplate can be applied to the items of an ItemsControl.

2.

If you want to have the SelectedItem property kept in sync across multiple selectors, what property must you set?

Answers

1.

The DataTemplate could be declared inline using the ItemTemplate property. It could also be declared in a Resources collection and applied either by key or type. Finally, a DataTemplateSelector can be used to apply the template based on advanced runtime conditions.

2.

You must set the IsSynchronizedWithCurrentItem to True.

Activities

  1. Besides ObjectDataProvider, WPF provides an XmlDataProvider. Spend some time researching this other data source provider.

  2. Move the DataTemplate declared in this chapter into a resources collection. Hint: You may have to reorganize some other resources as well.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.255.87