【问题标题】:How do you sync a ListView and List model using MVVM?如何使用 MVVM 同步 ListView 和 List 模型?
【发布时间】:2014-02-06 14:30:07
【问题描述】:

我正在尝试使用 MVVM 模式来显示可以在 UI 中更新的对象列表。我还想要一些基本操作,例如添加/删除。现在我有一个简单的Customer 模型,它只是一个名字和姓氏。我将DataContext 分配给ViewModel。在ViewModel 类中,我有稍后将注入的“模型”(只是客户的List)。为了使底层List 模型保持最新,我在每次访问List 时都用ObservableCollection 包装它。在这样做时,它似乎并没有保持SelectedValue 处于活动状态,因为当您删除SelectedValue 时,它会将其设置为null 并清除ListView 中的选择。这意味着我需要一些手动跟踪(我希望避免这种情况)。 我试图将 ObservableCollection 作为成员变量保留到 ViewModel 类,但这只会复制底层数据列表,如果您从中添加/删除对象,它不会保持同步。

我还想避免使用 ObservableCollection 作为模型,因为这似乎更适合 ViewModel 数据绑定支持 (reference; see Using it in the model)。 有没有人这样做过,并找到了一个好方法来保持List 模型同步,同时使用ObservableCollection 将数据绑定到ListView

MainWindow.xaml

<Window x:Class="TestCollectionAndSelectedItem.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestCollectionAndSelectedItem"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:IndexConverter x:Key="IndexConverter" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="35*" />
            <RowDefinition Height="276*" />
        </Grid.RowDefinitions>
        <Button Content="Add" Command="{Binding Path=AddCustomer}"  Width="95" Margin="22,11,0,231" Grid.Row="1" HorizontalAlignment="Left" />
        <Label Content="First" Height="26" Width="80" HorizontalAlignment="Left" Margin="263,95,0,155" Grid.Row="1" />
        <TextBox Text="{Binding SelectedCustomer.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Height="31" Width="133" IsEnabled="True" Margin="345,93,25,152" Name="textBox1" Grid.Row="1" />
        <Label Content="Last" Height="26" Width="80" HorizontalAlignment="Left" Margin="263,139,0,111" Grid.Row="1" />
        <TextBox Text="{Binding SelectedCustomer.LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="133" Height="31" IsEnabled="True" Margin="345,137,25,108" Name="textBox2" Grid.Row="1" />
        <ListView SelectionMode="Single" ItemsSource="{Binding Customers}" SelectedValue="{Binding SelectedCustomer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="listviewNames" Margin="22,56,258,77" Grid.Row="1">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Number"
                            DisplayMemberBinding="{Binding RelativeSource={RelativeSource FindAncestor, 
                            AncestorType={x:Type ListViewItem}}, 
                            Converter={StaticResource IndexConverter}}" />
                        <GridViewColumn Header="Last" DisplayMemberBinding="{Binding Path=LastName}" Width="80"/>
                        <GridViewColumn Header="First" DisplayMemberBinding="{Binding Path=FirstName}" Width="80"/>
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
        <Button Command="{Binding Path=RemoveCustomer}" CommandParameter="{Binding ElementName=listviewNames, Path=SelectedIndex}" Content="Remove" HorizontalAlignment="Left" Margin="150,11,0,231" Width="95" Grid.Row="1" />
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
   // wont go here, just example
   List<Customer> customers = new List<Customer>() { 
      new Customer() { LastName = "Anderson", FirstName = "John" },
      new Customer() { LastName = "NoName", FirstName = "" } };

   public MainWindow()
   {
      InitializeComponent();
      DataContext = new ViewModel(customers);
   }
}

ViewModel.cs

public class ViewModel : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   private List<Customer> _customersModel; 
   private Customer _selectedCustomer;

   public DelegateCommand<object> AddCustomer { get; private set; }
   public DelegateCommand<int> RemoveCustomer { get; private set; }

   public ViewModel(List<Customer> listOfCustomers)
   {
      _customersModel = listOfCustomers;
      AddCustomer = new DelegateCommand<object>((a) => Add(), (a) => CanAdd());
      RemoveCustomer = 
         new DelegateCommand<int>((a) => Remove(a), (a) => CanRemove());
   }

   public ObservableCollection<Customer> Customers
   {
      get
      {
         return new ObservableCollection<Customer>(_customersModel);
      }
      set
      {
         _customersModel = new List<Customer>(value);
         OnPropertyChanged("Customers");
      }
   }

   public Customer SelectedCustomer
   {
      get { return _selectedCustomer; }
      set
      {
         if (_selectedCustomer != value)
         {
            _selectedCustomer = value;
            UpdateCommands();
            OnPropertyChanged("SelectedCustomer");
         }
      }
   }

   public void Add()
   {
      _customersModel.Add(new Customer() { FirstName = "", LastName = "" });
      UpdateCommands();
      OnPropertyChanged("Customers");
   }

   public Boolean CanAdd()
   {
      return _customersModel.Count < 8;
   }

   public void Remove(int selectedIndex)
   {
      _customersModel.Remove(_selectedCustomer);
      UpdateCommands();
      OnPropertyChanged("Customers");
   }

   public Boolean CanRemove()
   {
      return (_selectedCustomer != null) && 
             (_customersModel != null) &&
             (_customersModel.Count > 0);
   }

   public void UpdateCommands()
   {
      RemoveCustomer.RaiseCanExecuteChanged();
      AddCustomer.RaiseCanExecuteChanged();
   }

   protected void OnPropertyChanged(string name)
   {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
      {
         handler(this, new PropertyChangedEventArgs(name));
      }
   } 
}

IndexConverter.cs

public class IndexConverter : IValueConverter
{
   public object Convert(object value, Type TargetType, object parameter, CultureInfo culture)
   {
      ListViewItem item = (ListViewItem)value;
      ListView listView = 
         ItemsControl.ItemsControlFromItemContainer(item) as ListView;
      int index = 
         listView.ItemContainerGenerator.IndexFromContainer(item) + 1;
      return index.ToString();
   }

   public object ConvertBack(object value, 
      Type targetType, object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}

Customer.cs

public class Customer
{
   public String FirstName { get; set; }
   public String LastName { get; set; }
}

【问题讨论】:

    标签: c# wpf xaml data-binding mvvm


    【解决方案1】:

    我不建议直接在视图模型上公开您的域模型。如果可以,这可能表明您拥有anemic domain model。这很流行,但并不一定意味着它是正确的。

    您可以公开ObservableCollectionCustomerViewModelCustomerViewModel 将是一个 DTO,用于绑定到您的视图(以及具有可能增强底层模型的属性)。

    您需要运行适当的域逻辑以反映用户操作的时间点取决于您的 UI。例如,用户可能会进行所有更改以一次列出并表示所有更改,或者您可能需要在每个列表更改上运行域逻辑。无论哪种方式,这都应该由您的域驱动,然后根据域内的成功或失败更新 UI 以反映这些更改。

    因此,当激活视图模型时,您可能希望通过注入的应用程序服务或直接通过存储库从数据存储中检索所有客户。然后将这些客户映射到您的 DTO (CustomerViewModel) 并呈现给用户。然后,当在您的 UI 中选择保存选项时,位于您的视图模型上的动词会将这些 DTO 映射回您的域对象,并且您将通过应用程序服务或存储库保留这些更改。

    【讨论】:

    • 感谢 devdigital,我不直接公开我的域模型,正如您提到的,它包含在 ObservableCollection 中:public ObservableCollection&lt;Customer&gt; Customers。想法是立即将更改提交给模型,而不是在其他某个时间点(这只是设计决策)。
    • 这仍然暴露了域模型 - 相反,有一个 ObservableCollection。当用户将客户添加到集合(或删除等)时,然后调用您的域逻辑(通过应用程序服务或存储库),然后在成功时更新可观察集合以反映更改。
    • 嗯,客户列表将如何与此相关联,该列表将在 CustomerViewModel 中,还是代表一个客户的 CustomerViewModel。有代码示例吗?
    • CustomerViewModel 代表一个客户。您不需要维护 List。激活时,您从服务/存储库返回的集合构建 ObservableCollection。视图模型绑定到这个可观察的集合,并且在您的视图模型上的 AddCustomer 动词中,您运行您的域逻辑(例如为了立即保持更改),检查结果,然后更新您的可观察集合以反映更改.
    猜你喜欢
    • 2013-03-19
    • 2015-05-15
    • 2021-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 2017-05-15
    相关资源
    最近更新 更多