在 WPF/C# 中通知 observable collection 中的项目更改

Notify item changes in observable collection in WPF/C#

在我的 WPF 项目中,我有一个 Service EF模型定义为

public class Service
{
  public int ID
  public string Name
  public decimal Price
}

在我的视图模型中我有。

public class ReceiptViewModel : BindableBase
{
    private ObservableCollection<Service> _services;
    public ObservableCollection<Service> Services
    {
        get { return _services; }
        set { SetProperty(ref _services, value, () => RaisePropertyChanged(nameof(Total))); }
    }

    public decimal Total => Services.Sum(s => s.Price);
}

Total 绑定到我视图中的文本块,我的可观察对象 collection 绑定到内部带有文本框的 itemscontrol。

我希望每次用户从 UI 更改我的 collection 中的价格时,我的 Total 文本块都会更改。我怎样才能做到这一点?

这很简单,但很乏味:

  1. 创建一个实现 INotifyPropertyChanged
  2. ServiceViewModel
  3. 将数据库中的集合映射到视图模型的可观察集合 (ReceiptViewModel.Services)
  4. 侦听可观察集合的所有更改并附加到所有添加项的 PropertyChanged(并从所有已删除项分离,同时附加所有最初存在的项)
  5. 每当 ServiceViewModels 之一发生变化时,设置新的 ReceiptViewModel.Total(或者如果你将其设为计算 属性 则提高 ReceiptViewModel.PropertyChanged
  6. ServiceViewModel 或视图模型集合发生变化时也更新数据库

如果您改用 EventAggregator,则第 3 步可能 easier/simpler/more 明显,也可能不明显。

你应该自己实现它,我的可观察集合示例(你还需要订阅并在集合项更改时引发 Raise OnPropertyChanged(nameof(Total))),或者将我的 collectionEx 实现更改为引发集合更改事件.

    public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
  public ObservableCollectionEx(IEnumerable<T> initialData) : base(initialData)
  {
      Init();
  }

  public ObservableCollectionEx()
  {
      Init();
  }

  private void Init()
  {
      foreach (T item in Items)
         item.PropertyChanged += ItemOnPropertyChanged;

      CollectionChanged += FullObservableCollectionCollectionChanged;
  }

  private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  {
      if (e.NewItems != null)
      {
         foreach (T item in e.NewItems)
         {
            if (item != null)
               item.PropertyChanged += ItemOnPropertyChanged;
         }
      }

      if (e.OldItems != null)
      {
          foreach (T item in e.OldItems)
          {
              if (item != null)
                  item.PropertyChanged -= ItemOnPropertyChanged;
          }
      }
  }

    private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs e)
        => ItemChanged?.Invoke(sender, e);

    public event PropertyChangedEventHandler ItemChanged;
}

您的问题不是很清楚 - 您当前的实施情况如何?有什么错误吗?我们应该注意的任何断点分析?您的绑定是如何在 Xaml

中实现的

在提供进一步信息之前,这里是您所提供内容的通用答案。

我将对您的 ReceiptViewModel 进行以下更改,充分利用 Prism MVVM。

public class ReceiptViewModel : BindableBase
{
    private ObservableCollection<Service> _services;
    public ObservableCollection<Service> Services
    {
        get { return _services; }
        set { SetProperty(ref _services, value); }
    }

    private decimal _total;
    public decimal Total
    {
        get { return _total; }
        set { SetProperty(ref _total, Services.Sum(s => s.Price)); }
    }

    public DelegateCommand PriceChangedCommand {get; set;}
    
    public ReceiptViewModel()
    {
       PriceChangedCommand = new DelegateCommand(OnPriceChanged);
    }

    private void OnPriceChanged()
    {
       Total = Services.Sum(s=>s.Price);
    }
}

现在在你的 XAML 中(不确定当前的样子)所以这是一个例子

<!--Ensure your DataContext / ItemsSource is set correctly-->    
<TextBox Text={Binding Price}>
   <b:InteractionTriggers>
      <b:EventTrigger EventName="TextChanged">
          <prism:InvokeCommandAction Command={Binding DataContext.PriceChangedCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>
          <!--Your AncestorType will be your root (top level) e.g. Window/UserControl etc-->
      </b:EventTrigger>
   </b:InteractionTriggers>
</TextBox>

正如我所说,您可能不需要 RelativeSource 绑定 - 我不知道您当前的实现,因此我给出了一个更复杂的答案,可以在更大的场景中使用。但您可以尝试更简单的版本 -

<prism:InvokeCommandAction Command={Binding PriceChangedCommand} />

要将此类 行为 添加到 xaml,您可能需要使用“Blend for Visual Studio”(应该已经安装了 Visual Studio).或者通过在 Window/Usercontrol

中添加以下命名空间,尝试在不使用 Blend 的情况下执行此操作
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"

并安装以下 Nuget 包 - Microsoft.Xaml.Behaviors.Wpf

我试着听从@Haukinger 和@Bandook 的建议,然后我做了这个

模特

public class Service
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

扩展模型的视图模型

public class ServiceViewModel : Service, INotifyPropertyChanged
{
    private decimal price;
    public event PropertyChangedEventHandler PriceChanged;
    public event PropertyChangedEventHandler PropertyChanged;

    public ServiceViewModel()
    {
    }

    public ServiceViewModel(decimal value)
    {
        this.price = value;
    }

    public new decimal Price
    {
        get { return price; }
        set
        {
            price = value;
            // Call OnPropertyChanged whenever the property is updated
            OnPropertyChanged();
        }
    }

    // Create the OnPropertyChanged method to raise the event
    // The calling member's name will be used as the parameter.
    protected void OnPropertyChanged([CallerMemberName] string price = null)
    {
        PriceChanged?.Invoke(this, new PropertyChangedEventArgs(price));
    }
}

主视图模型

public class MainWindowViewModel : BindableBase
{
    private string _title = "Prism Application";
    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    }

    private string _firstName = "TABET AOUL";
    public string FirstName
    {
        get { return _firstName; }
        set { SetProperty(ref _firstName, value, ()=> RaisePropertyChanged(nameof(FullName))); }
    }

    private string _lastName = "Abdelkrim";
    public string LastName
    {
        get { return _lastName; }
        set { SetProperty(ref _lastName, value, () => RaisePropertyChanged(nameof(FullName))); }
    }

    public string FullName => $"{FirstName} {LastName}";

    private ObservableCollection<ServiceViewModel> _services = new ObservableCollection<ServiceViewModel>();
    public ObservableCollection<ServiceViewModel> Services
    {
        get { return _services; }
        set { SetProperty(ref _services, value, () => RaisePropertyChanged(nameof(Total))); }
    }

    public decimal Total => Services.Sum(s => s.Price);

    public DelegateCommand AddServiceCommand { get; set; }

    public void AddService()
    {
        var item = new ServiceViewModel { Name = "Service", Price = 100 };
        item.PriceChanged += Item_PriceChanged;
        Services.Add(item);
        RaisePropertyChanged(nameof(Total));
    }

    public MainWindowViewModel()
    {
        var item = new ServiceViewModel { Name = "Service", Price = 100 };
        item.PriceChanged += Item_PriceChanged;
        Services.Add(item);
        AddServiceCommand = new DelegateCommand(new Action(AddService));
    }

    private void Item_PriceChanged(object sender, PropertyChangedEventArgs e)
    {
        RaisePropertyChanged(nameof(Total));
    }
}

代码似乎工作正常,当我更改 collection 项目之一的价格时,我的 UI 中的总变化除了我添加新项目时,所以我手动调用了 RaisePropertyChanged在点击命令上。

欢迎大家有更优雅的解决方案