MVVM:完全な理解(+ WPF)パート1

この記事では、 MVVMパターンとそのWPFでの実装について、特定の数の学生に完全かつ最終的な理解をもたらすという私の経験に基づいています。 このパターンは、複雑さが増す例で説明されています。 最初に、特定の言語に関係なく使用できる理論的な部分、次に、WPFおよびわずかにPrismを使用してレイヤー間の通信を実装するためのいくつかのオプションを示す実用的な部分。



なぜMVVMパターンを使用するのですか? これは余分なコードです! 同じことをはるかに明確かつ率直に書くことができます。



私は答えます:小さなプロジェクトでは、簡単なアプローチが機能します。 しかし、彼が少し大きくなると、プログラムのロジックがインターフェイスに広がって、プロジェクト全体がモノリシックなもつれに変わります。これは解くよりも書き直しやすいです。 明確にするために、次の2つの写真を見ることができます。





画像1:MVVMを使用しないコード。





画像2:MVVMを使用したコード。



前者の場合、プログラマーは「このポートとこのポートを接続するだけで、なぜこれらすべてのクリップとラベルが必要なのですか?」という言葉で、単にパッチコードでいくつかのスロットを接続します。 2番目のケースでは、何らかのテンプレートアプローチを使用します。



例題1のパターンの検討:結果の結論と2つの数字の加算



方法論:



「ModelFirst」のアプローチを使用してプログラムを作成する方法。





したがって、私たちのタスクは、結果の出力に2つの数値を追加するプログラムを作成することです。 最初のポイントを満たします:モデルを作成します。 モデルは、何らかの創造的な努力またはその創造的な能力の使用を必要とする可能性があるプログラム内の場所です。



ただし、創造性はリソース集約型の思考プロセスであり、過度の使用は避けてください。 まず、過労しないようにします。 第二に、仲間のプログラマー(そして数週間後、あなた自身)がコードをちらっと見て、過度にエキサイティングな時間を過ごした後、創造的な思考の流れに従うことを強制されないようにします。 あなたが突然創造性を適用することに成功したすべての場所-あなたは詳細なコメントを提供する必要があります。 ところで、この創造性を実現するのに役立つプログラミングパターンは数多くありますが、当てはまりません。



したがって、私たちの問題のモデルは、結果を返す数値の加算です。 原則として、モデルは状態を保存しません。 すなわち これは、静的クラスの静的メソッドによって実装できます。 このようなもの:



static class MathFuncs { public static int GetSumOf(int a, int b) => a + b; }
      
      





次のステップ(ModelFirst方法論を参照)は、ビューを作成するか、より簡単にインターフェイスを描画することです。 これも創造性を含む部分です。 しかし、再び、彼と一緒に行き過ぎないでください。 ユーザーは、インターフェイスの予期しないことにショックを受けるべきではありません。 インターフェイスは直感的でなければなりません。 ビューには、ラベルを付けることができる3つのテキストフィールドが含まれます:番号1、番号2、金額。



最後のステップは、VMを介してビューとモデルを接続することです。 VMは、クリエイティブな要素をまったく含むべきではない場所です。 すなわち パターンのこの部分は、Viewによって皮肉なことに決定され、「ビジネスロジック」を含むべきではありません。 Viewからの条件性とはどういう意味ですか? つまり、ビューに3つのテキストフィールドがある場合、またはデータを入力/出力する必要がある3つの場所がある場合、VM(基板の一種)には、これらのデータが受け入れる/提供するプロパティが少なくとも3つ必要です。



したがって、2つのプロパティはビュー番号1と2を取り、3番目のプロパティはモデルを呼び出してプログラムのビジネスロジックを実行します。 VMはそれ自体で数字を追加することはなく、このアクションのモデルを呼び出すだけです! これはVMの機能です-View(ユーザーからの入力を受け取り、出力を提供するだけです)と、すべての計算が行われるモデルを接続します。 パズルの絵を描くと、次のようなものが得られます。





画像3:例1のスキーム



緑はビューで、3つの緑の点が3つのテキストボックスです。 青は、これら3つの緑のドットが釘付け(境界)されているVMであり、赤の雲は計算を処理するモデルです。



WPFでの例1の実装



特に、WPFはMVVMパターンの「ハードウェアサポート」を実装しています。 ビューはXAMLで実装されます。 すなわち 緑のレイヤー(ビュー)はXAMLで記述されます。 緑のドットはテキストフィールドになります。 また、青の線に接続する緑の線は、バインドメカニズムによって実装されます。 緑の破線-VMオブジェクトを作成してDataContextビュープロパティに割り当てると、ビューとVM全体が接続されます。



ビューを描画します:



 <Window .... xmlns:local "clr-namespace: MyNamespace"> <!--      VM --> <Window.DataContext> <local:MainVM/> <!--   VM     View --> </Window.DataContext> <StackPanel> <!--Binding, ,       VM --> <!--UpdateSourceTrigger,   ,     VM    --> <TextBox Width="30" Text="{Binding Number1, UpdateSourceTrigger=PropertyChanged}"> <TextBox Width="30" Text="{Binding Number2, UpdateSourceTrigger=PropertyChanged}"> <!--Mode=OneWay        --> <TextBox Width="30" Text="{Binding Number3, Mode=OneWay}" IsReadOnly="True"> </StackPanel>
      
      





ここで、テクニックの最後のポイント、つまりVMを実装します。 VMがビューを「自動的に」更新するには、INotifyPropertyChangeインターフェイスを実装する必要があります。 VMで何かが変更され、データを更新する必要があるという通知をViewが受信するのは、それを介してです。



これは次のように行われます。



 public class MainVM : INotifyPropertyChange { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
      
      





次に、3つの必要なプロパティをVMに提供します。 (VMとViewを接続するための要件は、これらがパブリックプロパティでなければならないということです)



 private int _number1; public int Number1 { get {return _number1;} set { _number1 = value; OnPropertyChanged("Number3"); //  View  ,    } } private int _number2; public int Number2 { get {return _number2;} set { _number1 = value; OnPropertyChanged("Number3"); } }
      
      





最後のプロパティは、VMとモデルの間の青い点線です。



 //   ,   View  ,   Number1  Number2 public int Number3 { get; } => MathFuncs.GetSumOf(Number1, Number2);
      
      





MVVMパターンを使用した本格的なアプリケーションを実装しました。



例2のパターンの検討:



次に、タスクを複雑にします。 プログラムには、数字を入力するためのテキストボックスがあります。 値のコレクションを持つListBoxがあります。 「追加」ボタン。クリックすると、テキストフィールドの数値が値のコレクションに追加されます。 リストボックスで選択した番号をコレクションから削除する削除ボタン。 コレクション内のすべての値の合計を含むテキストボックス。





画像4:例2のインターフェイス



方法論に従って、最初にモデルを開発する必要があります。 これで、モデルをステートレスにすることはできず、状態を保存する必要があります。 そのため、モデルには要素のコレクションが含まれます。 今がその時です。 次に、コレクションに特定の番号を追加する操作は、モデルの責任です。 VMはモデルの内部に入り込むことはできず、モデルのコレクションに個別に番号を追加することはできません。 そうでなければ、カプセル化の原則に違反します。 まるでドライバーがガソリンタンクなどに燃料を入れなかったようです。 -しかし、彼はボンネットの下に登り、シリンダーに燃料を直接噴射しました。 つまり、「コレクションに番号を追加する」という方法があります。 これらは2つです。 3番目:モデルはコレクションの値の合計を提供し、同様にINotifyPropertyChangedインターフェイスを介してその変更について通知します。 モデルの純度に関する紛争を提起するのではなく、通知のみを使用します。



すぐにモデルを実装しましょう:



アイテムのコレクションは、サブスクライバーに変更を通知する必要があります。 モデルのみが変更できるように、読み取り専用にする必要があります。 アクセス制限はカプセル化の原則の実装であり、a)わかりにくいデバッグの状況を誤って作成しないように、b)フィールドが外部から変化しないという自信を植え付けます-再び、デバッグを容易にするために、厳密に遵守する必要があります。



さらに、この方法でDelegateCommandの Prismをさらに有効化するため、独立したINotifyPropertyChangeの実装の代わりにBindableBaseをすぐに使用しましょう。 これを行うには、NuGetを介してPrism.Wpfライブラリを接続します(執筆時点では6.3.0)。 したがって、OnPropertyChanged()はRaisePropertyChanged()に変更されます。



 public class MyMathModel : BindableBase { private readonly ObservableCollection<int> _myValues = new ObservableCollection<int>(); public readonly ReadOnlyObservableCollection<int> MyPublicValues; public MyMathModel() { MyPublicValues = new ReadOnlyObservableCollection<int>(_myValues); } //         public void AddValue(int value) { _myValues.Add(value); RaisePropertyChanged("Sum"); } //  ,         public void RemoveValue(int index) { //      -   if (index >= 0 && index < _myValues.Count) _myValues.RemoveAt(index); RaisePropertyChanged("Sum"); } public int Sum => MyPublicValues.Sum(); // }
      
      





テクニックに従って-ビューを描画します。 この前に、いくつかの必要な説明。 ボタンとVMリンクを作成するには、DelegateCommandを使用する必要があります。 純粋なMVVMの場合、このフォームでイベントとコードを使用することは許可されていません。 使用済みのイベントは、チームで構成する必要があります。 しかし、ボタンの場合、そのようなフレームは必要ありません。 その特別なプロパティはCommandです。



さらに、DelegateCommandを使用して追加する番号は、VMにバインドしませんが、VMを混乱させず、直接コマンド呼び出しとパラメーターの使用の非同期を回避するために、このDelegateCommandにパラメーターとして渡します。 結果の図、特に赤い線で囲まれた場所に注意してください。





画像5:例2のスキーム



ここでは、ビューへのバインドは、ビュー<=> ViewModelではなく、ビュー<=>ビューで発生します。 これを実現するために、2番目のタイプのバインディングが使用されます。バインディングの実行先の要素とそのプロパティの名前が示されます-「{Binding ElementName = TheNumber、Path = Text}」。



 <Window ...> <Window.DataContext> <local:MainVM/> <!--  DataContext --> </Window.DataContext> <DockPanel> <!--      --> <StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> <TextBox x:Name="TheNumber" Width="50" Margin="5"/> <Button Content="Add" Margin="5" Command="{Binding AddCommand}" CommandParameter="{Binding ElementName=TheNumber, Path=Text}"/> </StackPanel> <!--  --> <TextBox DockPanel.Dock="Bottom" Text="{Binding Sum, Mode=OneWay}" Margin="5"/> <!--     --> <Button DockPanel.Dock="Right" VerticalAlignment="Top" Content="Remove" Width="130" Margin="5" Command="{Binding RemoveCommand}" CommandParameter="{Binding ElementName=TheListBox, Path=SelectedIndex}"/> <!--  --> <ListBox x:Name="TheListBox" ItemsSource="{Binding MyValues}"/> </DockPanel> </Window>
      
      





次に、ViewModelを実装します。



 public class MainVM : BindableBase { readonly MyMathModel _model = new MyMathModel(); public MainVM() { //         View _model.PropertyChanged += (s, e) => { RaisePropertyChanged(e.PropertyName); }; AddCommand = new DelegateCommand<string>(str => { //    -  VM int ival; if (int.TryParse(str, out ival)) _model.AddValue(ival); }); RemoveCommand = new DelegateCommand<int?>(i => { if(i.HasValue) _model.RemoveValue(i.Value); }); } public DelegateCommand<string> AddCommand { get; } public DelegateCommand<int?> RemoveCommand { get; } public int Sum => _model.Sum; public ReadOnlyObservableCollection<int> MyValues => _model.MyPublicValues; }
      
      





注意が重要です! モデルからの通知の転送に関して。 VMは、金額の変更を個別に通知することはできません。 彼女は、メソッドを呼び出した後、モデル内で何を変更すべきか、まったく変更すべきかどうかを正確に知るべきではありません。 VMのモデルはブラックボックスである必要があります。 すなわち ユーザー入力とアクションをモデルに送信する必要があります。また、モデル内で何かが変更された場合(モデル自体が通知する必要がある)、Viewにのみ通知する必要があります。



MVVMパターンを使用して2番目の本格的なアプリケーションを実装し、ObservableCollection、DelegateCommand、Viewバインディング<View <=> View、およびViewでの通知の転送に精通しました。



All Articles