WPFで計算可能なプロパティのバインディングを実装する別の方法

WPFにプロジェクトがあり、その中に2つのプロパティPriceとQuantity、および計算可能なプロパティTotalPrice = Price * QuantityがあるViewModelがあるとします



コード
public class Order : BaseViewModel { private double _price; private double _quantity; public double Price { get { return _price; } set { if (_price == value) return; _price = value; RaisePropertyChanged("Price"); } } public double Quantity { get { return _quantity; } set { if (_quantity == value) return; _quantity = value; RaisePropertyChanged("Quantity"); } } public double TotalPrice {get { return Price*Quantity; }} } public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { var propertyChanged = PropertyChanged; if (propertyChanged != null) propertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
      
      











コードで価格が変更されると、ViewModelはRaisePropertyChanged( "Price")イベントを発生させて価格の変更をViewに通知するため、価格の変更は自動的にビューに表示されます。 計算されたTotalPriceは、RaisePropertyChanged( "TotalPrice")を呼び出さないため、Viewでは変更されません。 RaisePropertyChanged( "Price")およびRaisePropertyChanged( "Quantity")が呼び出されるのと同じ場所でRaisePropertyChanged( "TotalPrice")を呼び出すことができますが、TotalPriceが価格と数量に依存するという情報を多くの場所で広めたくはありません。これに関する情報を1か所に保管したいと思います。 このために、 人々はさまざまな依存関係マネージャーを作成しますが、実際に必要な最小限のコードをみましょう。



デザインの観点からロジックが属さない場所に到達する標準的な方法は、イベントを使用することです。 額のアプローチは、2つのOnPriceChangedおよびOnQuantityChangedイベントを作成することです。 これらのイベントが発生したら、RaisePropertyChanged(「TotalPrice」)を実行します。 これらのイベントは、ViewModelコンストラクターでサブスクライブします。 その後、TotalPriceが価格と数量に依存するという情報は、1つの場所-コンストラクター(まあ、または必要に応じて別のメソッド)にあります。



タスクを少し簡単にしましょう。Priceが変更されたときに起動するPropertyChangedイベントが既にあるので、それを使用します。



  public void RegisterPropertiesDependencies(string propertyName, List<string> dependenciesProperties) { foreach (var dependencyProperty in dependenciesProperties) { this.PropertyChanged += (sender, args) => { if (args.PropertyName == dependencyProperty) RaisePropertyChanged(propertyName); }; } }
      
      





  RegisterPropertiesDependencies("TotalPrice", new List<string> { "Price", "Quantity"});
      
      







このコードにはいくつかの欠点があります。まず、プロパティ名を文字列にステッチすることをお勧めしません。ラムダから取得することをお勧めします。次に、計算されたプロパティの外観がより複雑な場合、このコードは機能しません。たとえば、TotalCost = o .OrderProperties.Orders.Sum(o => o.Price * o.Quantity)。



コードOrderPropertiesおよびViewModel。 ここではすべてが明らかです、あなたは見ることができません
 public class OrderProperties : BaseViewModel { private ObservableCollection<Order> _orders = new ObservableCollection<Order>(); public ObservableCollection<Order> Orders { get { return _orders; } set { if (_orders == value) return; _orders = value; RaisePropertyChanged("Orders"); } } } public class TestViewModel : BaseViewModel { public double Summa {get { return OrderProperties.Orders.Sum(o => o.Price*o.Quantity); }} public OrderProperties OrderProperties { get { return _orderProperties; } set { if (_orderProperties == value) return; _orderProperties = value; RaisePropertyChanged("OrderProperties"); } } private OrderProperties _orderProperties; }
      
      











イベントを通じて、コレクション内の各アイテムの価格と数量の変更をサブスクライブします。 ただし、アイテムはコレクションに追加/削除できます。 コレクションを変更するときは、RaisePropertyChanged(「TotalPrice」)を呼び出す必要があります。 アイテムを追加するときは、価格と数量の変更にサブスクライブする必要があります。 また、OrderPropertiesでは誰かが新しいコレクションを割り当てることも、ViewModelでは新しいOrderPropertiesを割り当てることもできることを考慮する必要があります。



結果はこのコードです:



  public void RegisterElementPropertyDependencies(string propertyName, object element, ICollection<string> destinationPropertyNames, Action actionOnChanged = null) { if (element == null) return; if (actionOnChanged != null) actionOnChanged(); if (element is INotifyPropertyChanged == false) throw new Exception(string.Format("      {0}, ..    INotifyPropertyChanged", element.GetType())); ((INotifyPropertyChanged)element).PropertyChanged += (o, eventArgs) => { if (destinationPropertyNames.Contains(eventArgs.PropertyName)) { RaisePropertyChanged(propertyName); if (actionOnChanged != null) actionOnChanged(); } }; } public void RegisterCollectionPropertyDependencies<T>(string propertyName, ObservableCollection<T> collection, ICollection<string> destinationPropertyNames, Action actionOnChanged = null) { if (collection == null) return; if (actionOnChanged != null) actionOnChanged(); foreach (var element in collection) { RegisterElementPropertyDependencies(propertyName, element, destinationPropertyNames); } collection.CollectionChanged += (sender, args) => { RaisePropertyChanged(propertyName); if (args.NewItems != null) { foreach (var addedItem in args.NewItems) { RegisterElementPropertyDependencies(propertyName, addedItem, destinationPropertyNames, actionOnChanged); } } }; }
      
      







この場合、OrderProperties.Orders.Sum(o => o.Price * o.Quantity)の場合、次のように使用する必要があります。



 RegisterElementPropertyDependencies("Summa", this, new[] {"OrderProperties"}, () => RegisterElementPropertyDependencies("Summa", OrderProperties, new[] {"Orders"}, () => RegisterCollectionPropertyDependencies("Summa", OrderProperties.Orders, new[] { "Price", "Quantity" })));
      
      







このコードをさまざまな状況でテストしました。要素の数量を変更し、新しいOrderとOrderPropertiesを作成し、最初にOrderを変更し、次に数量など、コードは正常に機能しました。



PSところで、私はKnockoutのスタイルでObservablesに目を向けることをお勧めします。 そこで、プロパティが依存するものを指定する必要はありません。計算するためのアルゴリズムを渡すだけです。

fullName = new ComputedValue(()=> FirstName.Value + "" + ToUpper(LastName.Value));

ライブラリは、式ツリーを分析し、FirstNameメンバーとLastNameメンバーへのアクセスを確認し、依存関係を制御します。 プロパティを計算するためのアルゴリズムを変更した後、依存関係の名前を変更し忘れるリスクはなくなります。 確かに、彼らはライブラリがわずかに変更され、ネストされたコレクションを追跡しないと言いますが、フリータイムの車がある場合は、ソース(前のリンクから入手可能)を開いてファイルを少し操作したり、独自の式ツリーアナライザーバイクを記述したりできます。



PPSガベージコレクションについて:要素のファイナライザにメッセージ出力を追加すると、ウィンドウが閉じられると、すべての要素がガベージコレクタによって収集されることがあります(ViewModelには子要素へのリンクがあり、子要素にはイベントハンドラ内のViewModelへのリンクがあるにもかかわらず) これは、WPFがPropertyChangedEventManagerを通じて弱いイベントを使用して、DataBinding中のメモリリークを修正するためです。 詳細はリンクで読むことができます: [1][2][3]



All Articles