6

ViewModel用とProduct用の 2 つのクラスがあります。Product クラスにはLine Totalというプロパティがあり、ViewModel クラスにはTotal Amountというプロパティがあります。Product クラスは DataGrid にバインドされ、ユーザーが数量を挿入すると、その後Line Totalが自動的に更新されます。

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");
            }
        }
    }

子であるProductクラスは次のとおりです。

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

            }

        }
}

私の質問は、TotalAmountがすべての Products [Line Total]の合計をどのように計算できるかということです。子Productsが 親ViewModelにTotalAmountを更新するように通知するにはどうすればよいですか?

何かのようなもの:

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

4 に答える 4

9

これを達成する方法は、行の合計がユーザーによって編集されるたびに、および製品が に追加または削除されるたびに、合計金額を再計算することObservableCollectionです。

は、新しい行の合計が設定されたときにイベントをProduct実装INotifyPropertyChangedして発生させるため、 はそのイベントを処理して合計金額を再計算できます。PropertyChangedViewModel

ObservableCollectionCollectionChangedにはアイテムが追加または削除されたときに発生するイベントがあるため、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>
于 2013-01-31T22:00:41.933 に答える
2

のプロパティがいつ更新されるかをParentViewModel気にする場合は、そのイベントをサブスクライブする必要があります。ChildModelPropertyChanged

ただし、 Collection がある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");
    }
}
于 2013-02-01T13:56:11.770 に答える
1

bernd_rauschの答えは正しい方向に進んでいると思います。基本的な質問は、なぜTotalAmountをViewModelに格納するのかということです。唯一の理由は、パフォーマンスに影響を与えるほど多くの製品があることである可能性があります。ただし、このシナリオでも、値の一貫性を保つように注意する必要があります。

最も安全な方法は、TotalAmountをその場で計算するTotalAmountプロパティを作成することです。次に、Changedイベントをチェーンします。

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");
  }

}

リセットの場合は無視しました。しかし、これはあなたに正しい方向性を与えるはずだと思います。計算結果をキャッシュしたい場合でも、このメソッドを使用して、変更ハンドラーの1つでリセットされる内部レイジー値によってキャッシュを実行します。

于 2013-02-01T14:29:35.680 に答える
0

UI の TotalAmount の値は、NotifyPropertyChanged イベントが発生するように TotalAmount を設定した場合にのみ更新されると思います。これを機能させるには、すべての製品の PropertyChangedEvent をリッスンする必要があり、製品のコレクションまたは LineTotal が変更されたときに、TotalAmount を _TotalAmount とは異なる値に設定する必要があります。

しかし、このコードを理解するのは非常に困難です。なぜ変数 (_TotalAmount) に読み込まれるたびに計算される値 (TotalAmount) を格納するのかが明確ではありません。また、_TotalAmount がゼロに設定されていないため、正しい値ではありません。

于 2012-10-02T14:28:03.083 に答える