【问题标题】:WPF MVVM: How to reflect changes of ObservableCollection in the UIWPF MVVM:如何在 UI 中反映 ObservableCollection 的变化
【发布时间】:2018-12-03 15:26:54
【问题描述】:

我在 WPF 方面相对较新,我正在尝试了解 MVVM 模式以及数据绑定如何与 ObservableCollection 一起工作,以便构建我正在使用 MVVM 处理的应用程序。我创建了一个具有 MainWindow 的应用程序示例,其中根据用户按下的按钮显示不同的视图 (UserControl)。总体思路是,用户可以访问数据库中某些元素的数据(例如:客户、产品等),并且能够添加新元素、编辑或删除现有元素。

因此,有一个 CustomerView 及其 CustomerViewModel 和一个 ProductView 及其 ProductViewModel。此外,还有两个代表模型的类(Customer.cs 和 Product.cs)。项目结构显示here

MainWindow.xaml 如下:

<Window.Resources>
    <DataTemplate DataType="{x:Type viewModels:CustomerViewModel}">
        <views:CustomerView DataContext="{Binding}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type viewModels:ProductViewModel}">
        <views:ProductView DataContext="{Binding}"/>
    </DataTemplate>
</Window.Resources>

<Grid >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="20*"/>
        <ColumnDefinition Width="80*"/>
    </Grid.ColumnDefinitions>

    <StackPanel Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="btnCustomers" Click="btnCustomers_Click" Content="Customers" Width="80" Height="50" Margin="10"/>
        <Button x:Name="btnProducts" Click="btnProducts_Click" Content="Products" Width="80" Height="50" Margin="10"/>
    </StackPanel>

    <Grid Grid.Column="1">
        <ContentControl Grid.Column="0" Content="{Binding}"/>
    </Grid>
</Grid>

以及 MainWindow.xaml.cs 背后的代码:

public partial class MainWindow : Window
{
    public CustomerViewModel customerVM;
    public ProductViewModel productVM;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void btnCustomers_Click(object sender, RoutedEventArgs e)
    {
        if (customerVM == null)
        {
            customerVM = new CustomerViewModel();
        }
        this.DataContext = customerVM;
    }

    private void btnProducts_Click(object sender, RoutedEventArgs e)
    {
        if (productVM == null)
        {
            productVM = new ProductViewModel();
        }
        this.DataContext = productVM;
    }
}

最后CustomerView.xaml如下:

<UserControl.Resources>
    <viewModel:CustomerViewModel x:Key="customerVM"/>
    <!-- Styling code here...-->
</UserControl.Resources>

<Grid DataContext="{StaticResource ResourceKey=customerVM}">
    <Grid.RowDefinitions>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="7*"/>
        <RowDefinition Height="3*"/>
    </Grid.RowDefinitions>

    <Grid Grid.Row="0">
        <TextBlock Text="Customers" FontSize="18"/>
    </Grid>

    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5*"/>
            <ColumnDefinition Width="5*"/>
        </Grid.ColumnDefinitions>

        <ComboBox x:Name="cmbCustomers" Grid.Column="0" VerticalAlignment="Top"
                  IsEditable="True"
                  Text="Select customer"
                  ItemsSource="{Binding}"
                  DisplayMemberPath="FullName" IsSynchronizedWithCurrentItem="True">
        </ComboBox>

        <StackPanel Grid.Column="1" Margin="5">
            <StackPanel Orientation="Horizontal">
                <TextBlock Grid.Column="0" Text="Id:" />
                <TextBlock Grid.Column="1" x:Name="txtId" Text="{Binding Path=Id}" FontSize="16"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Grid.Column="0" Text="Name:" />
                <TextBlock Grid.Column="1" x:Name="txtFirstName" Text="{Binding Path=FirstName}" FontSize="16"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Grid.Column="0" Text="Surname:" />
                <TextBlock Grid.Column="1" x:Name="txtLastName" Text="{Binding Path=LastName}" FontSize="16"/>
            </StackPanel>
        </StackPanel>
    </Grid>

    <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center">
        <Button x:Name="btnAddNew" Content="Add New" Click="btnAddNew_Click"/>
        <Button x:Name="btnDelete" Content="Delete Customer" Click="btnDelete_Click"/>
    </StackPanel>
</Grid>

和 CustomerViewModel.cs:

public class CustomerViewModel : ObservableCollection<Customer>
{
    public CustomerViewModel()
    {
        LoadCustomers();
    }

    private void LoadCustomers()
    {
        for (int i = 1; i <= 5; i++)
        {
            var customer = new Customer()
            {
                Id = i,
                FirstName = "Customer_" + i.ToString(),
                LastName = "Surname_" + i.ToString()
            };
            this.Add(customer);
        }
    }

    public void AddNewCustomer(int id)
    {
        var customer = new Customer()
        {
            Id = id,
            FirstName = "Customer_" + id.ToString(),
            LastName = "Surname_" + id.ToString()
        };
        Add(customer);
    }
}

请注意,ProductView.xaml 和 ProductViewModel.cs 是相似的。 目前,当用户按下“客户”或“产品”按钮时 MainWindow,然后显示相应的 View 并加载集合 根据 LoadCustomers(或 LoadProducts)方法,该方法由 ViewModel 的构造函数。此外,当用户从 ComboBox,则其属性显示正确(即 Id、Name 等)。这 问题是当用户添加新的(或删除现有的)元素时。


问题 1:哪种方法是更新已更改的 Observable 的正确和最佳方式 一个元素的集合并在 UI 中反映它的变化(组合框、属性等)?

问题 2:在测试这个项目的过程中,我注意到 ViewModels(因此是 LoadCustomers 和 LoadProducts 方法)被调用两次。但是,它仅在用户按下客户或 产品按钮分别。它是否也通过 XAML 数据绑定调用?是 这是最佳的实现方式?

【问题讨论】:

  • 您不能将 UserControl 的 XAML 中顶级 Grid 的 DataContext 分配给“私有”视图模型实例(customerVM 资源)。如果这样做,则在外部分配给 UserControl 的 DataContext 将被忽略。除此之外,&lt;views:CustomerView DataContext="{Binding}"/&gt; 等价于&lt;views:CustomerView /&gt;
  • 好吧,老实说,我还没有 100% 了解数据绑定机制和 MVVM 模式。关于顶级 Grid 中的 DataContext,这是我在 WPF 书籍示例中看到的。更具体地说,它提到:“通过设置顶层网格的DataContext,我们将使子控件继承DataContext进行数据绑定”。你说的外部添加的DataContext是指在MainWindow类后面的代码中设置“this.DataContext = customerVM”的时候?
  • 是的,就是这样。

标签: c# wpf xaml mvvm observablecollection


【解决方案1】:

您的第一个问题基本上是 UX 问题,没有正确或“最佳”的方法。您最终肯定会使用某种ItemsControl,但使用哪一种在很大程度上取决于您希望用户如何与之交互。

对于第二个问题,您的代码中有一些错误:

  1. &lt;viewModel:CustomerViewModel x:Key="customerVM"/&gt; 实例化一个视图模型,除了主应用程序创建的视图模型

  2. Grid DataContext="{StaticResource ResourceKey=customerVM}" 然后使用这个“本地”视图模型,忽略从主应用程序继承的视图模型

这就是为什么你看到构造函数触发了两次,你正在构造两个实例!消除本地 VM,不要在网格上分配 DC。其他问题:

  • &lt;views:ProductView DataContext="{Binding}"/&gt; DataContext 分配完全没有必要,因为它在数据模板中,它的数据上下文已经设置好了
  • &lt;ContentControl Grid.Column="0" Content="{Binding}"/&gt; 呸,你应该有一个带有 property 的“MainViewModel”。不要让它成为整个数据上下文

  • 缺少用于单击按钮的命令(与上面的项目符号相关)

【讨论】:

  • 我从网格中删除了 DataContext="{StaticResource ResourceKey=customerVM}" 你是对的,构造函数没有被触发两次。但是,我有点困惑。您说“ 实例化了一个新的视图模型,除了主应用程序创建的视图模型”。我认为在将 MainWindow 的 DataContext 设置为新的 ViewModel(来自后面的代码)之后,为了在 MainWindow 的 ContentControl 中显示相应的视图(UserControl),我们必须如上所述在 XAML 中声明它。这是错的吗?
  • 关于您指出的问题:1)您能否解释一下我在 ContentControl 上犯的大错误是什么,以及如何按照您建议的方式完成? 2)我从按钮点击事件中排除了代码,以使我的帖子尽可能小。在实际应用程序中读取、添加、编辑和删除数据库元素工作正常。主要问题是如何在从数据库中添加或删除后(即当 ObservableCollection 更改时)刷新绑定到显示 View 的 ViewModel,而无需切换到另一个 View 并返回。
【解决方案2】:

MVVM 中的列表需要 3 种更改通知:

  1. 更改列表项的每个属性的通知。
  2. 更改显示列表的属性的通知,以防整个实例必须被替换(这很常见,因为 3)
  3. 如果元素被添加到集合中或从集合中删除,则更改通知。这是 ObservableCollection 唯一关心的事情。不幸的是,没有 Addrange 选项,因此批量操作将 lsmwap 带有通知的 GUI。这就是Nr。 2 在那里。

作为高级选项,请考虑公开 CollectionView 而不是原始 Collection。 WPF GUI 元素不绑定到原始集合,只绑定到 CollectionViews。但是如果你不给他们一个,他们会自己创造一个。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-03-29
    • 2015-12-24
    • 2018-09-23
    • 1970-01-01
    • 2018-08-04
    • 2023-03-08
    • 2012-09-23
    • 1970-01-01
    相关资源
    最近更新 更多