Click here to Skip to main content
15,886,110 members
Articles / Desktop Programming / WPF

ListView, ComboBox, and ObservableCollection<T>

Rate me:
Please Sign up or sign in to vote.
4.36/5 (8 votes)
3 Feb 2010MIT5 min read 82.1K   4.9K   29   1
An article on WPF data binding using ObservableCollection.

The Edit Model Dialog

In the GeometryViz3D [^] project, I created a dialog, named EditModelDialog, for the user to maintain the points and lines of a 3D geometry model.

Image 1

Figure 1. The Edit Model Dialog

The dialog consists of two ListView controls and six Button controls. The Points ListView (at the top) and the Lines ListView (at the bottom) show all points and lines of the model, respectively. The Point 1 and Point 2 columns of the Lines ListView are ComboBox controls that allow you to choose the two end points of a line. The number of items in the ComboBox controls is the same as that in the Points ListView. When you add a new point in the Points ListView, the point will immediately appear in the ComboBox controls. And, when you modify a property of a point, the corresponding item in the ComboBox controls should also be refreshed.

As the names suggest, the Add Point, Add Line, Delete Point, and Delete Line buttons are for adding a point, adding a line, deleting a point, and deleting a line, respectively.

In this article, I will discuss how to implement the dialog using the MVVM pattern, and how to make sure the items in the ComboBox controls are in sync with those in the Points ListView.

CoreMVVM

CoreMVVM [^] is a framework that makes it easier for you to apply the MVVM pattern. The CoreMVVM classes and interfaces used in the GeometryViz3D [^] project include ViewModelBase, DelegateCommand, UIVisualizerService, IOpenFileService, and ISaveFileService. Please visit the CoreMVVM website [^] for details.

The View Models

The ViewModel class of the dialog, EditModelViewModel, defines a few properties and commands. The most interesting properties are Lines and Points. The Lines property is an ObservableCollection of LineViewModel, and the Points property, an ObservableCollection of PointViewModel. A LineViewModel also has an ObservableCollection of PointViewModel, representing the points that can be chosen from. Both PointViewModel and LineViewModel inherit from ElementViewModel, which, in turn, inherits from the ViewModelBase class from the CoreMVVM framework.

EditModelViewModel also defines six ICommand properties to be bound to the Button controls of the dialog.

Image 2

Figure 2. The View Models

Data Binding

First of all, in order to bind the properties of a ViewModel to a View, we need to set the ViewModel to the DataContext property of the View, which is done by the UIVisualizerService of the CoreMVVM framework.

First, we register the EditModelDialog with the UIVisualizerService.

C#
ViewModelBase.ServiceProvider.RegisterService<IUIVisualizerService>(
                              new UIVisualizerService());
IUIVisualizerService service = 
  ViewModelBase.ServiceProvider.GetService<IUIVisualizerService>();

service.Register("EditModelDialog", typeof(EditModelDialog));

Then, we call the ShowDialog() method on the UIVisualizerService object to show the dialog. The ViewModel is passed into the ShowDialog() method as a parameter, which is set to the DataContext property of the EditModelDialog, allowing us to bind the properties of EditModelViewModel to various controls of the EditModelDialog.

C#
private void EditModel()
{
    EditModelViewModel vm = new EditModelViewModel(Model);
    bool? result = m_uiVisualService.ShowDialog("EditModelDialog", vm);

    if (result.HasValue && result.Value)
    {
        m_model = vm.Model;
        OnPropertyChanged("Model");
    }
}

The Points ListView

The ItemsSource property of the ListView is bound to the Points property of the ViewModel, an ObservableCollection of PointViewModel objects. The points are shown in a GridView with five columns, binding to the ID, X, Y, Z, and Label properties of PointViewModel, respectively.

XML
<ListView Margin="3" 
          ItemsSource="{Binding Points}" 
          SelectedValue="{Binding SelectedPoint}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.View>
        <GridView>
            <GridView.Columns>
                <GridViewColumn Header="ID" 
                                Width="80">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding ID}" Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="X" Width="80">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding X}"  Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Y" Width="80">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Y}"  Margin="-6, 0, -6, 0" />
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Z" Width="80">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Z}"  Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Label" Width="80">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Label}"  Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </GridView.Columns>
        </GridView>
    </ListView.View>
</ListView>

The Lines ListView

The Lines ListView is bound to the Lines property of EditModelViewModel, an ObservableCollection of LineViewModel objects, also shown in a GridView. Both the Point 1 and Point 2 columns are bound to the AvailablePoints property of LineViewModel, whose getter method simply returns the Points property of EditModelViewModel. So, all ComboBox controls in the Point 1 and Point 2 columns are effectively bound to the Points property of EditModelViewModel. I was hoping that when the Points property is modified, the ComboBox controls will get updated automatically.

XML
<ListView Margin="3" 
          ItemsSource="{Binding Lines}"
          SelectedValue="{Binding SelectedLine}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.View>
        <GridView>
            <GridView.Columns>
                <GridViewColumn Header="ID" Width="100">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding ID}"  Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Point 1" Width="120">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding Path=AvailablePoints}"
                                      SelectedValue="{Binding StartPoint}"
                                       Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Point 2" Width="120">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding AvailablePoints}" 
                                      SelectedValue="{Binding EndPoint}"
                                       Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Color" Width="120">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding Colors}"
                                      SelectedValue="{Binding Color}"
                                       Margin="-6, 0, -6, 0"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </GridView.Columns>
        </GridView>
    </ListView.View>
</ListView>

Keeping Data in Sync

With the ViewModel classes and the XAML code in place, the DropDown lists of the ComboBox controls are refreshed accordingly when you add or delete a point. However, when you modify a point, e.g., changing the X value of a point, the DropDown lists still show the old value. Why?

My first thought was that when the X value of a point is changed, we should trigger the PropertyChanged event notifying that the AvailablePoints of the LineViewModel objects have been changed. To achieve that, the EditModelViewModel needs to handle the PropertyChanged event of all PointViewModel objects, so that whenever a property of a point is changed, the EditModelViewModel will raise the PropertyChanged event for the Points property, which will be handled by all LineViewModel objects by raising the PropertyChanged event for the AvailablePoints property, hoping that the ComboBox controls that bind to the AvailablePoints property will get refreshed.

It works, in a sense: the DropDown lists get updated when we modify a property of PointViewModel, but always one step later. For example, after we change the coordinate of a point from (0, 0, 0) to (5, 0, 0), the DropDown lists still show (0, 0, 0). And, after we change the point to (5, 6, 0), the DropDown lists still show (5, 0, 0). What happens?

It seems that ComboBox controls bound to an ObservableCollection<T> object update themselves only when they receive the CollectionChanged event from the ObservableCollection<T> object, and the CollectionChanged event is triggered only when an element is added or removed from the collection. Modifying an element of the collection won’t cause the CollectionChanged event to be triggered. Is it possible to force the event to be triggered?

From MSDN, we can see that the OnCollectionChanged() method is just what we want: it raises the CollectionChanged event. The only problem is that it is protected. In order to call it, we have to write a class inheriting from ObservableCollection<T>, with a public method whose only job is to call the OnCollectionChanged() method.

C#
public class ElementCollection<T> : ObservableCollection<T>
{
    public void UpdateCollection()
    {
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                            NotifyCollectionChangedAction.Reset));
    }
}

Then, we change the type of the EditModelViewModel.Points and LineViewModel.AvailablePoints properties to ElementCollection<PointViewModel>, and it solves the problem!

Points of Interest

The MVVM is a powerful WPF design pattern that allows us to unit-test our applications under the skin (XAML files). The model and ViewModel classes are all unit-testable.

Adding or removing an element from an ObservableCollection<T> object will cause the CollectionChanged event to be triggered, and therefore, the controls bound to the collection will get updated automatically. However, modifying an element of an ObservableCollection<T> object won’t get controls bound to it to be updated automatically. To achieve that, we have to write a class inheriting from it and call the protected OnCollectionChanged() method to trigger the CollectionChanged event.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Technical Lead Rockwell Automation
Singapore Singapore
He is a Software Engineer at Rockwell Automation Asia Pacific Business Center, working on RSLogix 5000. Prior to joining Rockwell Automation, he had worked for Sybase for 8 years and was the original architect of the PowerBuilder Native Interface and the PowerBuilder .NET Compiler that can compile PowerBuilder applications to .NET Windows Forms or Web Forms applications. The programming languages he has used or is using intensively include C#, C++, C and 8086 assembly.

Wu XueSong's Blog

Comments and Discussions

 
GeneralMy vote of 5 Pin
daveywc28-Feb-12 0:46
daveywc28-Feb-12 0:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.