简体   繁体   English

WPF MVVM:如何在UI中反映ObservableCollection的更改

[英]WPF MVVM: How to reflect changes of ObservableCollection in the UI

I am relatively new in WPF and am trying to understand the MVVM pattern and how data-binding works with ObservableCollection, in order to build the application I am working on with MVVM. 我在WPF中相对较新,并且正在尝试了解MVVM模式以及ObservableCollection如何进行数据绑定,以便构建我正在使用MVVM处理的应用程序。 I have created a sample of my application that has a MainWindow where, depending on which button the user presses, a different View (UserControl) is displayed. 我创建了一个具有MainWindow的应用程序示例,根据用户按下哪个按钮,将显示不同的视图(UserControl)。 The general idea is that the user will have access to the data of some elements from a database (eg: Customers, Products, etc.) and will be able to add new and edit, or delete, existing ones. 总体思路是,用户将可以访问数据库中某些元素的数据(例如:客户,产品等),并且能够添加新元素以及编辑或删除现有元素。

So, there is a CustomerView , with its CustomerViewModel, and a ProductView , with its ProductViewModel respectively. 因此,分别有一个CustomerView和它的CustomerViewModel,以及一个ProductView和它的ProductViewModel。 Also, there are two classes (Customer.cs & Product.cs) that represent the Models. 此外,还有两个表示模型的类(Customer.cs和Product.cs)。 The structure of the project is displayed here . 项目的结构显示在此处

The MainWindow.xaml is as follows: 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>

and the code behind MainWindow.xaml.cs: 以及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;
    }
}

Finally, the CustomerView.xaml is as follows: 最后,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>

and the CustomerViewModel.cs: 和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);
    }
}

Please note that the ProductView.xaml & ProductViewModel.cs are similar. 请注意,ProductView.xaml和ProductViewModel.cs是相似的。 Currently, when the user presses the "Customers" or the "Products" button of the MainWindow, then the respective View is displayed and the collections are loaded according to the LoadCustomers (or LoadProducts) method, which is called by the ViewModel's constructor. 当前,当用户按下MainWindow的“客户”或“产品”按钮时,将显示相应的视图,并根据LoadCustomers(或LoadProducts)方法加载集合,该方法由ViewModel的构造函数调用。 Also, when the user selects a different object from the ComboBox, then its properties are displayed correctly (ie Id, Name, etc.). 同样,当用户从组合框选择其他对象时,其属性也会正确显示(即ID,Name等)。 The problem is when the user adds a new (or deletes an existing) element. 问题是当用户添加新(或删除现有)元素时。


Question 1 : Which is the correct and best way to update a changed Observable Collection of an element and reflect its changes in the UI (Combobox, properties, etc.)? 问题1 :更新元素的已更改可观察集合并在UI(组合框,属性等)中反映其更改的正确和最佳方法是什么?

Question 2 : During testing this project I noticed that the constructor of the ViewModels (consequently the LoadCustomers & LoadProducts method) are called twice. 问题2 :在测试该项目期间,我注意到ViewModels的构造函数(因此是LoadCustomers&LoadProducts方法)被调用了两次。 However, it is only called when the user presses the Customers or the Products button respectively. 但是,仅当用户分别按下“客户”或“产品”按钮时才调用它。 Is it also called via the XAML data binding? 是否也通过XAML数据绑定来调用? Is this the optimum implementation? 这是最佳实现吗?

Your first question is basically a UX one, there is no correct or "best" way. 您的第一个问题基本上是用户体验,没有正确或“最佳”的方法。 You'll definitely end up using some sort of ItemsControl , but which one depends heavily on how you want your users to interact with it. 您肯定会最终使用某种ItemsControl ,但是哪种很大程度上取决于您希望用户如何与之交互。

To your second question, you have a few mistakes in your code: 第二个问题,您的代码有一些错误:

  1. <viewModel:CustomerViewModel x:Key="customerVM"/> Instantiates a new view model, apart from the one that the main application created <viewModel:CustomerViewModel x:Key="customerVM"/>实例化一个新的视图模型,除了主应用程序创建的模型之外

  2. Grid DataContext="{StaticResource ResourceKey=customerVM}" Then uses this "local" view model, ignoring the inherited one from the main application Grid DataContext="{StaticResource ResourceKey=customerVM}"然后使用此“本地”视图模型,而忽略了从主应用程序继承的模型

That's why you see the constructor fire twice, you are constructing two instances! 这就是为什么您看到构造函数触发两次,而您正在构造两个实例的原因! Eliminate the local VM and don't assign the DC on the grid. 消除本地VM,不要在网格上分配DC。 Other issues: 其他事宜:

  • <views:ProductView DataContext="{Binding}"/> The DataContext assignment is total unnecessary, by virtue of being in the data template it's data context is already set up <views:ProductView DataContext="{Binding}"/> DataContext分配是完全不必要的,因为它位于数据模板中,因此已经设置了数据上下文
  • <ContentControl Grid.Column="0" Content="{Binding}"/> Yuck, you should have a "MainViewModel" with a property that this uses. <ContentControl Grid.Column="0" Content="{Binding}"/>好吧,您应该有一个“ MainViewModel”及其使用的属性 Don't make it be the whole data context 不要让它成为整个数据上下文

  • Lack of commands for your button clicks (related to the bullet above) 缺少用于单击按钮的命令(与上面的项目符号有关)

There is 3 kinds of Change Notification you need with Lists in MVVM: MVVM中的列表需要3种变更通知:

  1. Change Notificataions on every property of the list items. 更改列表项的每个属性上的通知。
  2. Change Notification on the property exposing the list, in case the whole instance has to be replaced (wich is common because of 3) 在必须替换整个实例的情况下,在暴露列表的属性上的“更改通知”(由于3而非常常见)
  3. Change Notification if elements are added to or removed from the collection. 如果元素添加到集合中或从集合中删除,则更改通知。 That is the only thing ObservableCollection takes care off. 那是ObservableCollection唯一需要注意的事情。 Unfortunately there is no Addrange option, so bulk operations wil lsmwap the GUI with Notifications. 不幸的是,没有Addrange选项,因此批量操作将通过通知将GUI打包。 That is what Nr. 那就是Nr。 2 is there for. 2在那里。

As advanced option, consider exposing the CollectionView rather then the raw Collection. 作为高级选项,请考虑公开CollectionView而不是原始Collection。 WPF GUI elements do not bind to raw Collections, only CollectionViews. WPF GUI元素不绑定到原始Collection,仅绑定到CollectionViews。 But if you do not hand them one, they will create one themself. 但是,如果您不交给他们一个,他们就会自己创造一个。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM