65.9K
CodeProject 正在变化。 阅读更多。
Home

ListView、ComboBox 和 ObservableCollection<T>

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.36/5 (8投票s)

2010年2月3日

MIT

5分钟阅读

viewsIcon

83266

downloadIcon

4934

一篇关于使用 ObservableCollection 进行 WPF 数据绑定的文章。

编辑模型对话框

GeometryViz3D [^] 项目中,我创建了一个名为 EditModelDialog 的对话框,用于用户维护 3D 几何模型的点和线。

图 1. 编辑模型对话框

该对话框包含两个 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 类和接口包括 ViewModelBaseDelegateCommandUIVisualizerServiceIOpenFileServiceISaveFileService。请访问 CoreMVVM 网站 [^] 获取详细信息。

视图模型

对话框的 ViewModel 类 EditModelViewModel 定义了一些属性和命令。最有趣的属性是 LinesPointsLines 属性是 LineViewModelObservableCollectionPoints 属性是 PointViewModelObservableCollection。一个 LineViewModel 也包含一个 PointViewModelObservableCollection,表示可供选择的点。PointViewModelLineViewModel 都继承自 ElementViewModel,而 ElementViewModel 又继承自 CoreMVVM 框架中的 ViewModelBase 类。

EditModelViewModel 还定义了六个 ICommand 属性,用于绑定到对话框的 Button 控件。

图 2. 视图模型

数据绑定

首先,为了将 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() 方法,并设置为 EditModelDialogDataContext 属性,从而允许我们将 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

ListViewItemsSource 属性绑定到 ViewModel 的 Points 属性,这是一个 PointViewModel 对象的 ObservableCollection。这些点在 GridView 中显示,包含五列,分别绑定到 PointViewModelIDXYZLabel 属性。

<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 绑定到 EditModelViewModelLines 属性,这是一个 LineViewModel 对象的 ObservableCollection,也在 GridView 中显示。Point 1 和 Point 2 列都绑定到 LineViewModelAvailablePoints 属性,其 getter 方法只是返回 EditModelViewModelPoints 属性。因此,Point 1 和 Point 2 列中的所有 ComboBox 控件都有效地绑定到 EditModelViewModelPoints 属性。我希望当 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.PointsLineViewModel.AvailablePoints 属性的类型更改为 ElementCollection<PointViewModel>,问题就解决了!

关注点

MVVM 是一种强大的 WPF 设计模式,它允许我们在幕后(XAML 文件)对应用程序进行单元测试。模型和 ViewModel 类都是可进行单元测试的。

ObservableCollection<T> 对象中添加或移除元素将导致 CollectionChanged 事件被触发,因此,绑定到该集合的控件将自动更新。但是,修改 ObservableCollection<T> 对象的元素不会导致绑定到它的控件自动更新。为了实现这一点,我们必须编写一个继承自它的类,并调用 protected OnCollectionChanged() 方法来触发 CollectionChanged 事件。

© . All rights reserved.