ListView、ComboBox 和 ObservableCollection<T>
一篇关于使用 ObservableCollection 进行 WPF 数据绑定的文章。
编辑模型对话框
在 GeometryViz3D [^] 项目中,我创建了一个名为 EditModelDialog
的对话框,用于用户维护 3D 几何模型的点和线。
该对话框包含两个 ListView
控件和六个 Button
控件。点 ListView
(顶部)和线 ListView
(底部)分别显示模型的所有点和线。线 ListView
的 Point 1 和 Point 2 列是 ComboBox
控件,允许您选择一条线的两个端点。ComboBox
控件中的项目数量与点 ListView
中的项目数量相同。当您在点 ListView
中添加一个新点时,该点将立即出现在 ComboBox
控件中。并且,当您修改点的属性时,ComboBox
控件中相应的项目也应该刷新。
顾名思义,Add Point(添加点)、Add Line(添加线)、Delete Point(删除点)和 Delete Line(删除线)按钮分别用于添加点、添加线、删除点和删除线。
在本文中,我将讨论如何使用 MVVM 模式实现该对话框,以及如何确保 ComboBox
控件中的项目与点 ListView
中的项目保持同步。
CoreMVVM
CoreMVVM [^] 是一个使您更容易应用 MVVM 模式的框架。在 GeometryViz3D [^] 项目中使用的 CoreMVVM 类和接口包括 ViewModelBase
、DelegateCommand
、UIVisualizerService
、IOpenFileService
和 ISaveFileService
。请访问 CoreMVVM 网站 [^] 获取详细信息。
视图模型
对话框的 ViewModel 类 EditModelViewModel
定义了一些属性和命令。最有趣的属性是 Lines
和 Points
。Lines
属性是 LineViewModel
的 ObservableCollection
,Points
属性是 PointViewModel
的 ObservableCollection
。一个 LineViewModel
也包含一个 PointViewModel
的 ObservableCollection
,表示可供选择的点。PointViewModel
和 LineViewModel
都继承自 ElementViewModel
,而 ElementViewModel
又继承自 CoreMVVM 框架中的 ViewModelBase
类。
EditModelViewModel
还定义了六个 ICommand
属性,用于绑定到对话框的 Button
控件。
数据绑定
首先,为了将 ViewModel 的属性绑定到 View,我们需要将 ViewModel 设置为 View 的 DataContext
属性,这由 CoreMVVM 框架的 UIVisualizerService
完成。
首先,我们将 EditModelDialog
注册到 UIVisualizerService
。
ViewModelBase.ServiceProvider.RegisterService<IUIVisualizerService>(
new UIVisualizerService());
IUIVisualizerService service =
ViewModelBase.ServiceProvider.GetService<IUIVisualizerService>();
service.Register("EditModelDialog", typeof(EditModelDialog));
然后,我们调用 UIVisualizerService
对象的 ShowDialog()
方法来显示对话框。ViewModel 作为参数传递给 ShowDialog()
方法,并设置为 EditModelDialog
的 DataContext
属性,从而允许我们将 EditModelViewModel
的属性绑定到 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");
}
}
点 ListView
ListView
的 ItemsSource
属性绑定到 ViewModel 的 Points
属性,这是一个 PointViewModel
对象的 ObservableCollection
。这些点在 GridView
中显示,包含五列,分别绑定到 PointViewModel
的 ID
、X
、Y
、Z
和 Label
属性。
<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>
线 ListView
线 ListView
绑定到 EditModelViewModel
的 Lines
属性,这是一个 LineViewModel
对象的 ObservableCollection
,也在 GridView
中显示。Point 1 和 Point 2 列都绑定到 LineViewModel
的 AvailablePoints
属性,其 getter 方法只是返回 EditModelViewModel
的 Points
属性。因此,Point 1 和 Point 2 列中的所有 ComboBox
控件都有效地绑定到 EditModelViewModel
的 Points
属性。我希望当 Points
属性被修改时,ComboBox
控件会自动更新。
<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>
保持数据同步
当 ViewModel 类和 XAML 代码就位后,当您添加或删除一个点时,ComboBox
控件的 DropDown
列表会相应地刷新。但是,当您修改一个点时,例如,改变一个点的 X 值,DropDown
列表仍然显示旧值。为什么?
我的第一个想法是,当一个点的 X 值改变时,我们应该触发 PropertyChanged
事件,通知 LineViewModel
对象的 AvailablePoints
已经改变。为了实现这一点,EditModelViewModel
需要处理所有 PointViewModel
对象的 PropertyChanged
事件,这样每当一个点的属性改变时,EditModelViewModel
就会为 Points
属性引发 PropertyChanged
事件,这将被所有 LineViewModel
对象通过为 AvailablePoints
属性引发 PropertyChanged
事件来处理,希望绑定到 AvailablePoints
属性的 ComboBox
控件能够得到刷新。
从某种意义上说,它奏效了:当我们修改 PointViewModel
的属性时,DropDown
列表会更新,但总是慢一步。例如,在我们把一个点的坐标从 (0, 0, 0) 改为 (5, 0, 0) 之后,DropDown
列表仍然显示 (0, 0, 0)。而且,在我们把点改为 (5, 6, 0) 之后,DropDown
列表仍然显示 (5, 0, 0)。发生了什么?
看起来,绑定到 ObservableCollection<T>
对象的 ComboBox
控件只有在从 ObservableCollection<T>
对象接收到 CollectionChanged
事件时才会更新自己,而 CollectionChanged
事件只在集合中添加或移除元素时触发。修改集合中的元素不会导致 CollectionChanged
事件被触发。是否有可能强制触发该事件?
从 MSDN 中,我们可以看到 OnCollectionChanged()
方法正是我们想要的:它引发 CollectionChanged
事件。唯一的问题是它是 protected
。为了调用它,我们必须编写一个继承自 ObservableCollection<T>
的类,其中有一个 public
方法,其唯一的工作就是调用 OnCollectionChanged()
方法。
public class ElementCollection<T> : ObservableCollection<T>
{
public void UpdateCollection()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
然后,我们将 EditModelViewModel.Points
和 LineViewModel.AvailablePoints
属性的类型更改为 ElementCollection<PointViewModel>
,问题就解决了!
关注点
MVVM 是一种强大的 WPF 设计模式,它允许我们在幕后(XAML 文件)对应用程序进行单元测试。模型和 ViewModel 类都是可进行单元测试的。
从 ObservableCollection<T>
对象中添加或移除元素将导致 CollectionChanged
事件被触发,因此,绑定到该集合的控件将自动更新。但是,修改 ObservableCollection<T>
对象的元素不会导致绑定到它的控件自动更新。为了实现这一点,我们必须编写一个继承自它的类,并调用 protected
OnCollectionChanged()
方法来触发 CollectionChanged
事件。