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.
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.
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
.
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
.
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.
<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.
<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.
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.