通知/绑定父属性以计算子属性的总和

  • 本文关键字:属性 计算 绑定 通知 c# wpf
  • 更新时间 :
  • 英文 :


我有两个类,一个用于ViewModel,一个用于产品。产品类具有一个名为"行总计"的属性,而 ViewModel 类具有一个名为"总计金额"的属性。产品类绑定到数据网格和用户插入随后自动更新行总计的数量。

下面是 ViewModel 类:

public class ViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Product> products { get; set; }// the children
    private decimal _TotalAmount; 
    public decimal TotalAmount // <=== has to hold sum of [products.LineTotal]
    {
        get
        {
            return totalAmount;
        }
        set
        {
            if (value != _TotalAmount)
            {
                _TotalAmount = value;
                onPropertyChanged(this, "TotalAmount");
            }
        }
    }

这是一个子项的产品类:

public class Product : INotifyPropertyChanged
    {
        private decimal _LineTotal;
        public decimal LineTotal
        {
            get
            {
                return _LineTotal;
            }
            set
            {
                if (value != _LineTotal)
                {
                    _LineTotal = value;
                    onPropertyChanged(this, "LineTotal");
                }
            }
        }
}

我的问题是:总金额如何计算所有产品的总和[行总计]?子产品如何通知父视图模型更新总金额

像这样:

foreach(var product in Products)
{
     TotalAmount += product.LineTotal;
}
实现

此目的的一种方法是在用户每次编辑行总数以及每次从ObservableCollection中添加或删除产品时重新计算总金额。

由于Product实现INotifyPropertyChanged并在设置新行总计时引发PropertyChanged事件,因此ViewModel可以处理该事件并重新计算总量。

ObservableCollection具有在添加或删除项时引发的CollectionChanged事件,因此ViewModel还可以处理该事件并重新计算。(如果产品只能更改而不能由用户等添加/删除,则此部分不是真正必需的)。

你可以试试这个小程序,看看它是如何做到的:

代码隐藏

public partial class MainWindow : Window
{
    ViewModel vm = new ViewModel();
    public MainWindow()
    {
        InitializeComponent();
        vm.Products = new ObservableCollection<Product>
        {
            new Product { Name = "Product1", LineTotal = 10 },
            new Product { Name = "Product2", LineTotal = 20 },
            new Product { Name = "Product3", LineTotal = 15 }
        };
        this.DataContext = vm;
    }
    private void AddItem(object sender, RoutedEventArgs e)
    {
        vm.Products.Add(new Product { Name = "Added product", LineTotal = 50 });
    }
    private void RemoveItem(object sender, RoutedEventArgs e)
    {
        vm.Products.RemoveAt(0);
    }
}
public class ViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Product> _products;
    public ObservableCollection<Product> Products
    {
        get { return _products; }
        set
        {
            _products = value;
            // We need to know when the ObservableCollection has changed.
            // On added products: hook up eventhandlers to their PropertyChanged events.
            // On removed products: recalculate the total.
            _products.CollectionChanged += (sender, e) =>
            {
                if (e.NewItems != null)
                    AttachProductChangedEventHandler(e.NewItems.Cast<Product>());
                else if (e.OldItems != null)
                    CalculateTotalAmount();
            };
            AttachProductChangedEventHandler(_products);
        }
    }
    private void AttachProductChangedEventHandler(IEnumerable<Product> products)
    {
        // Attach eventhandler for each products PropertyChanged event.
        // When the LineTotal property has changed, recalculate the total.
        foreach (var p in products)
        {
            p.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == "LineTotal")
                    CalculateTotalAmount();
            };
        }
        CalculateTotalAmount();
    }
    public void CalculateTotalAmount()
    {
        // Set TotalAmount property to the sum of all line totals.
        TotalAmount = Products.Sum(p => p.LineTotal);
    }
    private decimal _TotalAmount;
    public decimal TotalAmount
    {
        get { return _TotalAmount; }
        set
        {
            if (value != _TotalAmount)
            {
                _TotalAmount = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("TotalAmount"));
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}
public class Product : INotifyPropertyChanged
{
    public string Name { get; set; }
    private decimal _LineTotal;
    public decimal LineTotal
    {
        get { return _LineTotal; }
        set
        {
            if (value != _LineTotal)
            {
                _LineTotal = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("LineTotal"));
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

XAML:

<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <DataGrid ItemsSource="{Binding Products}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" />
                <DataGridTextColumn Binding="{Binding LineTotal}" />
            </DataGrid.Columns>
        </DataGrid>
        <Button Click="AddItem">Add item</Button>
        <Button Click="RemoveItem">Remove item</Button>
        <TextBlock>
            <Run>Total amount:</Run>
            <Run Text="{Binding TotalAmount}" />
        </TextBlock>
    </StackPanel>
</Window>

如果ParentViewModel关心ChildModel上的属性何时更新,则应订阅其PropertyChanged事件。

但是,由于您有一个ChildModels集合,因此挂接PropertyChanged事件的处理程序应该在CollectionChanged事件中添加/删除。

// Hook up CollectionChanged event in Constructor
public MyViewModel()
{
    Products = new ObservableCollection<Product>();
    MyItemsSource.CollectionChanged += Products_CollectionChanged;
}
// Add/Remove PropertyChanged event to Product item when the collection changes
void Products_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
    if (e.NewItems != null)
        foreach(Product item in e.NewItems)
            item.PropertyChanged += Product_PropertyChanged;
    if (e.OldItems != null)
        foreach(Product item in e.OldItems)
            item.PropertyChanged -= Product_PropertyChanged;
}
// When LineTotal property of Product changes, re-calculate Total
void Product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "LineTotal")
    {
        TotalAmount = products.Sum(p => p.LineTotal);
        // Or if calculation is in the get method of the TotalAmount property
        //onPropertyChanged(this, "TotalAmount");
    }
}
我相信

bernd_rausch答案是正确的。基本问题是为什么要在视图模型中存储总金额?唯一的原因可能是您拥有太多的产品,以至于影响了性能。但即使在这种情况下,您也必须小心保持值一致。

最安全的方法是编写一个 TotalAmount 属性来动态计算 TotalAmount。然后链接"已更改"事件。

public class ViewModel : INotifyPropertyChanged
{
  ViewModel()
  {
    Products = new ObservableCollection<Product>();
    Products.CollectionChanged += OnProductsChanged;
  }
  public ObservableCollection<Product> Products { get; private set; }// the children
  public decimal TotalAmount { get { return Products.Select(p => p.LineTotal).Sum(); } }
  private void OnProductChanged(object sender, PropertyChangedEventArgs eventArgs)
  {
     if("LineTotal" != eventArgs.PropertyName)
           return;
     onPropertyChanged(this, "TotalAmount");
  }
  private void OnProductsChanged(object sender, NotifyCollectionChangeEventArgs eventArgs)
  {
     // This ignores a collection Reset...
     // Process old items first, for move cases...
     if (eventArgs.OldItems != null)
       foreach(Product item in eventArgs.OldItems)
         item.PropertyChanged -= OnProductChanged;
     if (eventArgs.NewItems != null)
       foreach(Product item in eventArgs.NewItems)
        item.PropertyChanged += OnProductChanged;

     onPropertyChanged(this, "TotalAmount");
  }
}

我忽略了重置情况。但我认为这应该给你正确的方向。如果您想缓存计算结果,我仍然会使用此方法,并通过在其中一个更改处理程序中重置的内部惰性值进行缓存。

我认为 UI 中 TotalAmount 的值仅在您设置 TotalAmount 以便触发 NotifyPropertyChanged 事件时更新。为此,您必须侦听所有产品的 PropertyChangedEvent,当产品的集合更改或 LineTotal 更改时,您必须将 TotalAmount 设置为与_TotalAmount不同的某个值。

但是这段代码真的很难理解:不清楚为什么要存储一个值,每次在变量(_TotalAmount)中读取(TotalAmount)时都会计算该值。由于_TotalAmount未设置为零,因此不是正确的值。

最新更新