2016-10-05 12 views
2

質問:バインドされたUI要素を更新して、リスト内の個々の項目のプロパティが値を変更した後にViewModelでのみ定義されたプロパティの値を表示する適切な方法。基になるモデルデータが変更されたときにViewModelで定義された通知プロパティの変更

INotifyPropertyChangedをクラスの中でリスト内の項目にすると、特定のデータがバインドされているUI要素のみが更新されます。 ListViewアイテムまたはDataGridセルと同様です。そしてそれはうまくいきます、それが私たちが望むものです。しかし、Excelのテーブルのように、合計行が必要な場合はどうすればよいでしょうか。その特定の問題についてはいくつかの方法がありますが、ここでの根底にある問題は、ViewModelでモデルのデータに基づいてプロパティを定義して計算することです。例:

public class ViewModel 
{ 
    public double OrderTotal => _model.order.OrderItems.Sum(item => item.Quantity * item.Product.Price); 
} 

いつ、どのように通知/更新/呼び出されますか?

さらに詳しい例を試してみましょう。

ここでXAMLこれはモデルである

<Grid> 
    <DataGrid x:Name="GrdItems" ... ItemsSource="{Binding Items}"/> 
    <TextBox x:Name="TxtTotal" ... Text="{Binding ItemsTotal, Mode=OneWay}"/> 
</Grid> 

です:

public class Item : INotifyPropertyChanged 
{ 
    private string _name; 
    private int _value; 

    public string Name 
    { 
     get { return _name; } 
     set 
     { 
      if (value == _name) return; 
      _name = value; 
      OnPropertyChanged(); 
     } 
    } 

    public int Value 
    { 
     get { return _value; } 
     set 
     { 
      if (value.Equals(_value)) return; 
      _value = value; 
      OnPropertyChanged(); 
     } 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    protected virtual void OnPropertyChanged(string propertyName = null) 
    { 
     PropertyChanged?.Invoke(this, new propertyChangedEventArgs(propertyName)); 
    } 
} 

public class Model 
{ 
    public List<Item> Items { get; set; } = new List<Item>(); 

    public Model() 
    { 
     Items.Add(new Item() { Name = "Item A", Value = 100 }); 
     Items.Add(new Item() { Name = "Item b", Value = 150 }); 
     Items.Add(new Item() { Name = "Item C", Value = 75 }); 
    } 
} 

とのViewModel:私は、これはコードを知っている

public class ViewModel 
{ 
    private readonly Model _model = new Model(); 

    public List<Item> Items => _model.Items; 
    public int ItemsTotal => _model.Items.Sum(item => item.Value); 
} 

は、簡略化の上に見えますが、それはの一部ですより大きく、難解な困難なアプリケーションです。

DataGridで項目の値を変更するときは、ItemsTotalプロパティでTxtTotalテキストボックスを更新する必要があります。

これまでのところ、ObservableCollectionの使用とCollectionChangedイベントの実装が含まれていました。

にモデルチェンジ:

public class Model: INotifyPropertyChanged 
{ 
    public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>(); 

    public Model() 
    { 
     Items.CollectionChanged += ItemsOnCollectionChanged; 
    } 

    . 
    . 
    . 

    private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
    { 
     if (e.NewItems != null) 
      foreach (Item item in e.NewItems) 
       item.PropertyChanged += MyType_PropertyChanged; 

     if (e.OldItems != null) 
      foreach (Item item in e.OldItems) 
       item.PropertyChanged -= MyType_PropertyChanged; 
    } 

    void MyType_PropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e.PropertyName == "Value") 
      OnPropertyChanged(nameof(Items)); 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    . 
    . 
    . 

} 

そしてへのviewmodelの変更:

public class ViewModel : INotifyPropertyChanged 
{ 
    private readonly Model _model = new Model(); 

    public ViewModel() 
    { 
     _model.PropertyChanged += ModelOnPropertyChanged; 
    } 

    private void ModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) 
    { 
     OnPropertyChanged(nameof(ItemsTotal)); 
    } 

    public ObservableCollection<Item> Items => _model.Items; 
    public int ItemsTotal => _model.Items.Sum(item => item.Value); 


    public event PropertyChangedEventHandler PropertyChanged; 

    [NotifyPropertyChangedInvocator] 
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
    { 
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 

このソリューションは動作しますが、それだけでより多くの雄弁な実装を持つ必要がありますハックの周りの仕事のように思えます。私のプロジェクトには、ビューモデルのこれらの合計プロパティのいくつかがあります。それは、更新するプロパティがたくさんあり、多くのオーバーヘッドのような感じのコードを書くことができます。

私はもっと多くの研究が必要ですが、私はこの質問を書いている間にいくつかの興味深い記事が出ました。私は、この問題を他のソリューションへのリンクで更新します。この問題は、私が思ったよりも一般的です。

+0

これは常にxamlにあったビットです。あなたはko.computedと似た何かを探しています。以下は、類似のことを行ういくつかの記事です。http://philchuang.com/mvvm-pain-points-chaineddependentcalculated-properties/ http://www.codeproject.com/Articles/375192/WPF-The-calculated-property-dependency -problem http://www.codeproject.com/Articles/422581/Knockout-Style-Observables-in-XAML –

答えて

0

あなたのプロジェクトはMVVMのように見えますが、私はそれが実際にはないと思います。レイヤーはありますが、モデルとビューモデルは取引の責任です。 MVVMの状況で純粋なものを保つ1つの方法は、ビューモデル以外のものにINotifyPropertyChangedを絶対に置かないことです。自分自身をモデルに入れると、ビューモデルの責任によってモデルが壊れてしまいます。同上ビュー(ただし、ビューにはINotifyPropertyChangedをスタックする傾向がありません)。ビューが単一のビューモデルに関連付けられているという前提を壊すのにも役立ちます。それはMVCの考え方からのクロスオーバーのように感じます。

私が言っていることは、概念的に始まる構造的な問題があるということです。たとえば、ビューモデルが子ビューモデルを持つことができない理由はありません。確かに、私はしばしば私がオブジェクトの強い階層を持っているときに役立つことがよくあります。 ItemとItemViewModelを持っています。そして、あなたの親オブジェクト(例えば、Parent)とParentViewModelが何であれ。 ParentViewModelには、ItemViewModel型の監視可能なコレクションがあり、子のOnPropertyChangedイベントにサブスクライブします(これは、合計プロパティのOnPropertyChangedを発生させます)。このようにして、ParentViewModelはプロパティの変更のUIを警告し、その変更が親モデルに反映される必要があるかどうかを判断できます(親データに集約を格納することがあります。計算されたフィールド(合計など)は、多くの場合、ViewModelにのみ存在します。

つまり、ViewModelは調整を処理します。あなたのViewModelはモデルデータのマスターであり、オブジェクト間のコミュニケーションはモデルを通してではなく、ビューモデルからビューモデルへと起こるはずです。これは、あなたのUIが、親のためのビューと子供のための別個に定義されたビューを持ち、それらが拘束されたビューモデルを介して通信するため、それらの独立した作品を保持できることを意味します。

それは意味がありますか?

それはのようになりたい:複雑です

public class ParentViewModel : INotifyPropertyChanged 
{ 
    private readonly Model _model; 

    public ParentViewModel(Model model) 
    { 
     _model = model; 
     Items = new ObservableCollection<ItemViewModel>(_model.Items.Select(i => new ItemViewModel(i))); 
     foreach(var item in Items) 
     { 
      item.PropertyChanged += ChildOnPropertyChanged; 
     } 
     Items.CollectionChanged += ItemsOnCollectionChanged; 
    } 

    private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
    { 
     if (e.NewItems != null) 
      foreach (Item item in e.NewItems) 
       item.PropertyChanged += ChildOnPropertyChanged; 

     if (e.OldItems != null) 
      foreach (Item item in e.OldItems) 
       item.PropertyChanged -= ChildOnPropertyChanged; 

     OnPropertyChanged(nameof(ItemsTotal)); 
    } 

    private void ChildOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) 
    { 
     if (e.PropertyName == "Value") 
      OnPropertyChanged(nameof(ItemsTotal)); 
    } 

    public ObservableCollection<ItemViewModel> Items; 
    public int ItemsTotal => Items.Sum(item => item.Value); 


    public event PropertyChangedEventHandler PropertyChanged; 

    [NotifyPropertyChangedInvocator] 
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
    { 
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 

を、しかし少なくとも、すべての合併症は、あなたのViewModelに含まれているし、そこから調整されています。

+0

Jacob-非常に興味深い、特に、「オブジェクト間のコミュニケーションは、モデルを通してではなく、ビューモデルからビューモデルに起こらなければならない」というコメントは、 。私はデザインパターンの理想を理解していますが、OPが何をしているのかについての具体的な利点は何ですか?あなたのソリューションは非常に複雑であることに注意してください。さらに、データ・モデルの変更が、ビューがインスタンス化されているかどうかには依存しないようにする必要がある場合はどうなりますか?表示されていないデータ操作でもモデルを作成する必要がありますか?フォームが余分な複雑さを正当化するほど重要であると思うのはなぜだろうか。 –

+0

問題は複雑です。これは、モデルオブジェクトではなく、単一の場所にすべての複雑さを置きます。確かに、私はここでオリジナルのプログラミングをしていない、私はちょうど上から元のメソッドを引っ張り、ViewModelから適用するように変更しました。他のすべてのオブジェクトは、バインディングを含めて単純なものになります。したがって、これは上記の元のソリューションからの単純化を表しています。あなたのUIアプリケーションがデータを変更している場合は、UIがそれ自身に踏み込まないようにViewModelを通過する方が良いでしょう。独断にする必要はありませんが、例外はまれです... –

0

これは私の目的のために十分に機能するようです。私は、このソリューションは、さまざまな形で、インターネット上で見つけることができると確信しています。

public class ChildNotifier<T> : INotifyPropertyChanged where T : INotifyPropertyChanged 
{ 
    private ObservableCollection<T> _list; 

    public ObservableCollection<T> List 
    { 
     get { return _list; } 
     set 
     { 
      if (Equals(value, _list)) return; 
      _list = value; 

      foreach (T item in _list) 
       item.PropertyChanged += ChildOnPropertyChanged; 

      OnPropertyChanged(); 
     } 
    } 

    protected ChildNotifier(IEnumerable<T> list) 
    { 
     _list = new ObservableCollection<T>(list); 
     _list.CollectionChanged += ItemsOnCollectionChanged; 
     foreach (T item in _list) 
      item.PropertyChanged += ChildOnPropertyChanged; 
    } 

    protected abstract void ChildOnPropertyChanged(object sender, propertyChangedEventArgs e); 

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

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

     OnPropertyChanged(); 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    [NotifyPropertyChangedInvocator] 
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
    { 
     PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 
関連する問題