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

画像



この記事では、例として、少し複雑なプログラム、つまり自動販売機を用意します。その実装は、インタビューの前にテストタスクとしてよく見られます。 複数のビューと1つのVMの相互作用が考慮され、逆もまた同様です。「最初に表示」アプローチが表示され、最終コードではなく、どの部分に必要な部分の説明( 自動販売機(プログラムコード)によるダウンロードリンクが表示されますが、作成プロセス全体が示されます)そして、最も重要なことは、一貫した思考の流れです。



しかし、その前に、非構造化プロジェクトのデバッグに慣れていない人が通常尋ねない質問、つまり「なぜMVVMパターンが必要なのですか?」



正式かつ簡潔に、MVVMパターンは主に責任を共有し、コードの可読性、管理性、保守性、およびテスト容易性を向上させるために使用されます。 ソフトウェア製品は、モデル(ドメインモデルとビジネスロジック)とインフラストラクチャコードで構成され、たとえば20%から80%の比率です。 インフラストラクチャコードは、Scaffoldingのように、シンプルで簡単、ほぼ自動である必要があります。 しかし、モデル...



インフラストラクチャコード全体に均等に分散するのではなく、1か所にグループ化することをお勧めします。 その後、それは読み取り可能です-すなわち。 さまざまな場所にあるイベントハンドラー、ダスティングダストのコントロールのヒープによってヒープ化されていない、サブジェクトエリアのプロセスのビジネスロジックを見ることができます。 それは制御可能です-すなわち たとえば、アクセス修飾子を1か所で変更することで、クライアントコードがプログラム全体の特定の係数を変更しないようにすることができます。 サポートされており、拡張可能です。 新しい要件に従ってプログラムを簡単に修正し、新しい機能を導入できます。 また、テスト容易性の向上により、単体テストと統合テストでモデルをカバーできるため、この最新の機能を導入したときに古い機能が失われることはありません。 そして、それが落ちた場合、顧客の受け入れではなく、すぐに気づいたでしょう。 締め切りの3日前に長い間考え抜かれた夜をデバッグしていた人々は、上記のすべての利点について尋ねません。



具体的には、MVVMはWPFによって「ハードウェアでサポートされている」ため、MVVPはMVPやMVCなどではなく、WPFで使用されます。 ViewはINotifyPropertyChange、Observableなどを理解します-プレゼンターなどを介して手動で更新する必要はありません。 たとえば、WinFormのMVPはより多くのインフラストラクチャコード、さらには手動コードを必要とし、そこで責任を共有することの利点は、大量のドラフト作業によって隠されていました。



挑戦する



タスクに戻りましょう。 彼女にはこの言葉があります:スナック/飲み物の販売のための自動販売機と人の相互作用をエミュレートするプログラムを作成すること。



プログラムインターフェースには以下が表示されます。



  1. ユーザーのウォレットの内容(最初は10枚のノート/同じ金種のコイン)とその購入。
  2. 機械の自動保管の内容(最初は100紙幣/同じ金種の硬貨)
  3. マシンで購入可能な製品のリスト(マシンでは、最初は各アイテム100ユニット)
  4. マシンの現在のローン(ユーザーがそこに投資した金額)


プログラムインターフェイスは次を許可する必要があります。



  1. ユーザーがマシンに資金を預ける
  2. 食料品の買い物
  3. 需要と受信、時には変化


プラスは次のとおりです。



  1. 価格付きの製品のリストは、コードで厳密に設定されていません
  2. 硬貨/紙幣の金種は、コードで固く設定されていません
  3. 正式なMVVMパターンへの準拠
  4. モデルクラスのフィールドとプロパティへの最小アクセスの設定
  5. 美しいデザイン!


後者を行う可能性は低いですが、他のすべては非常に重要です。



最初の部分では、Model Firstテクニックを使用しました。



  1. プログラムモデルを開発します。
  2. プログラムインターフェイスを描画します。
  3. インターフェイスとモデルをVMのレイヤーに接続します。


このアプローチの特徴は、モデルとその機能を事前に明確に提示する必要があることです。 外部で提供するプロパティとメソッド、インターフェイスとの相互作用の配置方法。 しかし、開発の最初の段階では、この相互作用が必要なのか、その相互作用が必要なのかさえわかりません。 TKで説明されている動作に加えて、追加のサポートポイントが必要です。 このようなサポートのポイントは、インターフェイスによって提供できます。 VMを表示します。 VMでは、クライアントコード、つまり モデルに表示する公開コード。 すなわち テクニックは次のとおりです。



「最初に表示」の手法:



  1. プログラムインターフェイスの描画-表示
  2. これらのビュー用のVMを開発し、クライアントコード(モデル呼び出しコード)を生成します
  3. モデルの相互作用のためのインターフェースを持ち、その構造と内部ロジックを実装する


朝のスケッチ、夕方のモデル。



ユーザーインターフェイスの作成にはMVVM固有の機能はほとんどありませんが、このポイントをバイパスすることはできないため、ポイント1に進みましょう。



インターフェイス作成



TKでは、ユーザーのウォレットと購入、およびマシンのインターフェースを表示する必要があることを読みました。 インターフェースを物理的に(つまり、異なるファイルで)2つの部分に分けましょう。1つはユーザー用、もう1つはマシン用です。 これにより、XAMLファイルが小さくなります。 大きなXAMLファイルの操作は(個人的には)不便です。 さらに、このようなパーティションにはコストがかかりません。WPFでは非常に簡単です。UserControlsのペア-UserView.xamlとAutomatView.xamlを作成し、メインビュー-MainView.xamlで使用します。 そして、それら(UserView.xamlおよびAutomatView.xaml)は、メインフォームのDataContextを使用します。 すなわち DataContextを指定しない場合、論理ツリーを上に移動し、自分が配置されているメインフォームのDataContextを見つけて使用するようです。



UserView.xamlから始めましょう。 ここでウォレットの内容と購入を表示する必要があります。 ショッピングは間違いなくリストボックスです。 財布は単なる数字ですか? 現金の量は? いや 作業明細書には、ユーザーが各宗派ごとに10の請求書を持っていることが記載されています。 すなわち これは、数量を示すさまざまな請求書のリストボックスでもあります。 リリースしましょう:



UserView.xaml:



<!-- / --> <ListBox ItemsSource="{Binding UserWallet}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Width="32" Height="32" Source="{Binding Icon}"></Image> <Label Content="{Binding Name}"/> <Label Content="{Binding Amount}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
      
      





リストボックス自体は現在存在しないUserWalletプロパティ(ユーザーのウォレット)にバインドされ、そのアイテムは存在しない名前(「5ルーブル」または「2ルーブル」など)、金額、アイコン(紙幣アイコン、紙幣または硬貨の場合) ) アイコンは、ToRの追加のポイント5である「美しいデザイン」を実現するための意図的に失敗した試みです。 ちなみに、この画像のペアをソリューションフォルダー「Images」のプロジェクトに追加します。 プロパティで、ビルドアクション:リソースを指定します。 それぞれ「Coin.png」と「Banknote.png」。



購入したリストボックスは根本的に変わりません(アイコンを追加しない限り)



UserView.xaml:



 <!----> <DockPanel> <Label DockPanel.Dock="Top" Content=" "/> <ListBox ItemsSource="{Binding UserBuyings}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Label Content="{Binding Name}"/> <Label FontWeight="DemiBold" Content="{Binding Price}"/> <Label Content="{Binding Amount}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </DockPanel>
      
      





予想通り、これをGridとUserControlの2つの列にまとめてみましょう。 そして、ユーザーの現金金額を追加します。



UserView.xaml:



 <UserControl ...> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <!----> <DockPanel> <Label DockPanel.Dock="Top" Content=" "/> <!----> <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"> <Label Content=" :"/> <Label Content="{Binding UserSumm}"/> </StackPanel> <!-- / --> <ListBox ... /> </DockPanel> <!----> <DockPanel Grid.Row="0" Grid.Column="1" .../> </Grid> </UserControl>
      
      





これで、ユーザーは準備完了です。 それでは、マシンのインターフェースの実装を開始しましょう。 作業明細書によれば、お金の保管と購入の可能性を示す必要があります-つまり ユーザーだけでなく。 したがって、再利用のためにこれらのDataTemplatesをUserView.xamlファイルから切り取ります。 これらのDataTemplatesは個別のファイルとしてレイアウトし、マージされたリソースディクショナリとして使用できますが、メインビューのリソースに配置するだけです。



MainView.xaml:



 <Window ...> <Window.Resources> <!--      /  --> <!--     DataType (  ) --> <DataTemplate DataType="{x:Type local:ProductVM}"> <StackPanel Orientation="Horizontal"> <Label Content="{Binding Name}"/> <Label FontWeight="DemiBold" Content="{Binding Price}"/> <Label Content="{Binding Amount}"/> </StackPanel> </DataTemplate> <!--      / --> <DataTemplate DataType="{x:Type local:MoneyVM}"> <StackPanel Orientation="Horizontal"> <Image Width="32" Height="32" Source="{Binding Icon}"></Image> <Label Content="{Binding Name}"/> <Label Content="{Binding Amount}"/> </StackPanel> </DataTemplate> </Window.Resources> <!--    ( )  VM - MainViewVM.cs --> <Window.DataContext> <local:MainViewVM/> </Window.DataContext> <!--    ,   ,  -   ( ) --> <!--   DataContext       DataContext   --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <local:UserView Margin="10" /> <local:AutomatView Grid.Column="1" Margin="10"/> </Grid> </Window>
      
      





DataTemplateのDataTypeに注意してください。 これは、WPFで次のことを行う非常に難しいことです。指定されたタイプのオブジェクト(この場合はProductVMまたはMoneyVM)が何らかの要素(この場合はListBoxItem)のコンテンツとして割り当てられると、このオブジェクトはこの要素のDataContextになり、このテンプレートが好むコンテンツ。 ProductVMまたはMoneyVMは、まだ作成していないこれらのテンプレートのVMです。 これまでに3つのVMをすべて作成できます。



MainViewVM.csファイル:



 public class MainViewVM : BindableBase { } public class ProductVM { } public class MoneyVM { }
      
      





はい、Prism(6.3.0、Wpfの下の7つはまだ機能しません)を接続し、BindableBaseからMainViewVMを継承します。



すなわち 繰り返しますが、ListBoxはListをItemsSourceとして使用します。 このシートの各要素に対して、ListBoxItemが作成され、このオブジェクトにはタイプProductVMのこのオブジェクトが割り当てられます。 WPFはProductVMタイプのDataTemplateを持っていることを認識し、このDataTemplateはListBoxItemをそのコンテンツとして割り当て、ProductVMオブジェクト自体がDataContextとして使用され、Bindingがそれに実装されます。 ProductVMだけでなくMoneyVMも配置されているItemsSource ListBoxとして配列を使用する場合(両方がBindableBaseなどの共通の基本クラスから継承される場合)、DataTemplatesはそれらに別々に適用されます!



AutomatView.xamlを実装するために残ります。



AutomatView.xaml:



 <UserControl ...> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <!----> <DockPanel Grid.Row="0" Grid.Column="1"> <Label DockPanel.Dock="Top" Content=""/> <!----> <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom"> <Label Content=":"/> <Label Content="{Binding Credit}"/> </StackPanel> <!----> <ListBox ItemsSource="{Binding AutomataBank}" /> </DockPanel> <!-- --> <DockPanel Grid.Row="0" Grid.Column="0"> <Label DockPanel.Dock="Top" Content=""/> <ListBox ItemsSource="{Binding ProductsInAutomata}"/> </DockPanel> </Grid> </UserControl>
      
      





TKをさらに読みます:プログラムは...マシンにお金を預け、購入し、変更を受け取ることを許可する必要があります。



ListBoxの[Machine Items]ボタンで各製品の横にボタンを添付すると、購入するボタンをクリックできます。



同様に、コインアクセプターのリストボックス「Money Depository」では、ユーザーがマシンにお金を入金するためのボタンを各紙幣/コインに取り付けることができます。



これらのボタンがユーザーに関連付けられたインターフェイスの一部に表示されないようにするには、必要なプロパティ「表示...」を設定する必要があります。



また、ローンを示すテキストフィールドの横に、「返品の変更」ボタンを作成できます。



必要な変更を行います。



 <!--      /  --> <DataTemplate DataType="{x:Type local:ProductVM}"> <StackPanel Orientation="Horizontal"> <Button Visibility="{Binding IsBuyVisible}" Command="{Binding BuyCommand}">+</Button> ... <!--      / --> <DataTemplate DataType="{x:Type local:MoneyVM}"> <StackPanel Orientation="Horizontal"> <Button Visibility="{Binding IsInsertVisible}" Command="{Binding InsertCommand}">+</Button> ... <!----> <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom"> <Button Command="{Binding GetChange}" Margin="5"> </Button> ...
      
      





すべて、ステージ番号1全体が終わりました。 VMの作成に進みます。



ViewModelの作成



MainViewVM、ProductVM、MoneyVMクラスを既に作成しています(まだ作成していない場合は作成します)。

ReSharperがあり、ファイルUserView.xamlおよびAutomatView.xamlのユーザーグリッドに次の行を追加する場合:



<Grid d:DataContext="{d:DesignInstance {x:Type local:MainViewVM}}">







これにより、WPFエディターにDataContextの種類が通知されます(ただし、これはランタイムに影響しません)。Alt+ Enterを使用して、対応するフィールドをVMクラスに追加できます。 ReSharperがない場合は、手でそれを行うことができます。



 public class MainViewVM : BindableBase { public int UserSumm { get; } public ObservableCollection<MoneyVM> UserWallet { get; } public ObservableCollection<ProductVM> UserBuyings { get; } public DelegateCommand GetChange { get; } public int Credit { get; } public ReadOnlyObservableCollection<MoneyVM> AutomataBank { get; } public ReadOnlyObservableCollection<ProductVM> ProductsInAutomata { get; } } public class ProductVM { public Visibility IsBuyVisible { get; } public DelegateCommand BuyCommand { get; } public string Name { get; } public string Price { get; } public int Amount { get; } } public class MoneyVM { public Visibility IsInsertVisible { get; } public DelegateCommand InsertCommand { get; } public string Icon { get; } public string Name { get; } public int Amount { get; } }
      
      





ご覧のとおり、3つのVMがほぼ自動的に作成されています。 これで、プロパティごとに順番に実装できます。 このようなクライアントコードを自由に記述できます。 例:UserSumm => _user.UserSumm; すなわち UserSummプロパティを持つUserモデルクラスの_userオブジェクトがあることを意味します。 そのようなクラスを作成しましょう。 これで、モデル、またはモデルとVMの間の接点が作成され、モデルの外部境界が形成されます。

今だけ小さな余談。



作業ステートメントには、「...モデルクラスのフィールドとプロパティへの最小限の必要なアクセス」を提供する必要があると記載されています(クライアントコードから)。 このような要件は、このTKにあるだけでなく、一般に、ソフトウェア構造の原則に優先する必要があります。 クライアントコードが誤って(または意図的に)モデルに侵入して、強制的に計画外の状態になることはありません。 さらに、あなたは現在、匿名の金銭取引に関連するコードを開発しています。 あなたは今、コードに誤りを導入していることを想像してください。全国のユーザーが600万ルーブルのコーヒーを無料で飲むと、この損失は法廷であなたに負担され、あなたはこのソフトウェア会社で働き、一生のDelphi +でエンコードすることを余儀なくされますCookieごとに1C。



一般に、モデルは複数のクラスで構成されます。 また、モデルの1つのクラス「A」から、モデルの別のクラス「B」のSomeMethod()メソッドを呼び出し、クライアントコードからこのB.SomeMethod()を呼び出せないことを確認する必要があります。



これを実現するために、クラスBをクラスAの内部プライベートクラスにし、外部に公開できるインターフェイスを実装できます...しかし、一般的に、そのような目的のために特別に提供されたソリューション-内部アクセス修飾子があります。 すなわち ソリューションの別のプロジェクトでモデルを選択するだけです。 このように、内部修飾子を使用して、モデルコードからクライアントコードを物理的に展開できます。 現在、この個別のモデルは、たとえばWebソリューションで簡単に使用できます。



クラスライブラリプロジェクトを作成し、VendingMachine.Modelという名前を付け、そこにUserモデルクラスを追加して、UserSummプロパティを作成します。



MainViewVMで、User型の_userプライベート変数を宣言し、コンストラクターで作成します。

MainViewVMファイル:



 public class MainViewVM : BindableBase { public MainViewVM() { _user = new User(); } public int UserSumm => _user.UserSumm; //... private User _user; }
      
      





私たちがお金に出くわす次のアイテム-UserWallet、これはMoneyVMのコレクションです。 TK:「...硬貨/紙幣の額面はコードに厳密に設定されていません。」



すなわち どこか(データベース、構成ファイル、Webサービスなど)からの特定の宗派(ルーブル、2、5、10)があります。 ユーザーとマシンが同じ種類の金種を持っている必要があり、それらの種類を突然作成することはできません(たとえば、8ルーブルの硬貨)。 作成を禁止する必要がある場合は、プライベートコンストラクターが適しています。 それでも特定の種類の金種が必要な場合は、工場方式が適切であり、そのような金種シートが作成されます。 または、マルチスレッドが計画されていない場合(ただし計画されていない場合)、静的リストを使用できます。 やってみましょう。



 //,   ,      ,     public struct Banknote { //,       public static readonly IReadOnlyList<Banknote> Banknotes = new[] { new Banknote("", 1, true), new Banknote(" ", 2, true), new Banknote(" ", 5, true), new Banknote(" ", 10, false), new Banknote(" ", 50, false), new Banknote(" ", 100, false), }; private Banknote(string name, int nominal, bool isCoin) { Name = name; Nominal = nominal; IsCoin = isCoin; } public string Name { get; } public int Nominal { get; } public bool IsCoin { get; } //  .    }
      
      





今二番目。 宗派がありますが、UserWalletには、そのような宗派/数量のペアの配列があります。 カジノのチップのスタックのように:1ドルのスタック、250ドルのチップのスタックなど。 このようなスタック(スタック)が必要です。



 public class MoneyStack { public MoneyStack(Banknote banknote, int amount) { Banknote = banknote; Amount = amount; } public Banknote Banknote { get; } public int Amount { get; } }
      
      





辞書などの構造を使用することもできますが、プログラマの本能から、reduce、increaseなどの関数が必要であることがわかります。 今は追加しません、なぜなら それらを呼び出すクライアントコードはありません。 そのようなコードはありませんが、これらの関数は追加しません。 イベントのようです-ハンドラーがあるまで、開発したコントロールにイベントを追加しません(たとえば、マウスでのダブルクリック)。 それ以外の場合は、数十のイベントを作成できますが、そのうちの2つまたは3つが必要です。 クラスについても同じことが言えます。多くの異なる外部関数を作成できますが、そのうちのいくつかが必要なだけです。



これに応じて、Userクラスを更新し、そこにUserWalletを追加します。 予想どおり、UserWalletはReadOnlyObservableCollectionになり、このコレクションが異なることを保証するために、プライベートコレクションになります。 さらに、作業指示書によると、ユーザーは初期化中に各金種の10個のメモを発行する必要があります。 ユーザーコンストラクターで行いましょう。



User.cs:



 public class User { public User() { //  _userWallet = new ObservableCollection<MoneyStack> (Banknote.Banknotes.Select(b => new MoneyStack(b, 10))); UserWallet = new ReadOnlyObservableCollection<MoneyStack>(_userWallet); } public ReadOnlyObservableCollection<MoneyStack> UserWallet { get; } private readonly ObservableCollection<MoneyStack> _userWallet; ... }
      
      





次に、MainViewVMコンストラクターを更新します。 なぜなら UserにはMoneyStackモデルクラスのオブジェクトのコレクションがあり、MainViewVKにはVMクラスのコレクション-MoneyVMがあります。その後、変換を行う必要があります。 MoneyVMで、MoneyStackを受け入れるコンストラクターを作成します。



次に、最初に、初期化中に、次にコレクションを変更するときに、適切なVMを追加する必要があります(モデルの変更は1つであるため、a.NewItems?



 public MainViewVM() { _user = new User(); //    UserWallet = new ObservableCollection<MoneyVM>(_user.UserWallet.Select(ms => new MoneyVM(ms))); //        ((INotifyCollectionChanged) _user.UserWallet).CollectionChanged += (s, a) => { if(a.NewItems?.Count == 1) UserWallet.Add(new MoneyVM(a.NewItems[0] as MoneyStack)); if (a.OldItems?.Count == 1) UserWallet.Remove(UserWallet.First(mv => mv.MoneyStack == a.OldItems[0])); }; }
      
      





MoneyVMで対応する変更を行います。 MoneyStackをパラメーターとして受け入れ、MoneyStackを読み取るためのプロパティに割り当てます-後続の検索の便宜上。 ボタンの可視性は、InsertCommandコマンドの存在に依存します。 また、画像、数量、紙幣名を返します。



 public class MoneyVM { public MoneyStack MoneyStack { get; } public MoneyVM(MoneyStack moneyStack) { MoneyStack = moneyStack; } public Visibility IsInsertVisible => InsertCommand == null ? Visibility.Collapsed : Visibility.Visible; public DelegateCommand InsertCommand { get; } public string Icon => MoneyStack.Banknote.IsCoin ? "..\\Images\\coin.jpg" : "..\\Images\\banknote.png"; public string Name => MoneyStack.Banknote.Name; public int Amount => MoneyStack.Amount; }
      
      





MainViewVMの実装を継続します。 次の行はObservableCollection UserBuyingsです。



UserBuyingsは、以前の設計と非常によく似て実装されています。 また、プライベートコンストラクターを使用してProductモデルのクラスを作成します。 同じように、プログラムで使用可能な製品のコレクションを作成します(データベースなどから)。 同様に、ProductStackを作成します。 そして、ProductStackからProductVMに変換するように。



Product.cs:



 public class Product { //,    web service public static IReadOnlyList<Product> Products = new List<Product>() { new Product("",12), new Product(" ", 25), new Product("",6), new Product("",23), new Product("",19), new Product("",670), }; private Product(string name, int price) { Name = name; Price = price; } public string Name { get; } public int Price { get; } } ProductStack.cs: public class ProductStack { public ProductStack(Product product, int amount) { Product = product; Amount = amount; } public Product Product { get; } public int Amount { get; } }
      
      





Userクラスで、ほぼ同じReadOnlyObservableCollectionを作成します。 デザイナーで現在10個のすべての製品名をユーザーに提供しない限り、 これはTKで指定されていません。



 public class User { public User() { ... //  UserBuyings = new ReadOnlyObservableCollection<ProductStack>(_userBuyings); } public ReadOnlyObservableCollection<ProductStack> UserBuyings { get; } private readonly ObservableCollection<ProductStack> _userBuyings = new ObservableCollection<ProductStack>(); ... }
      
      





それに応じてProductVMを更新します。



 public class ProductVM { public ProductStack ProductStack { get; } public ProductVM(ProductStack productStack) { ProductStack = productStack; } public Visibility IsBuyVisible => BuyCommand == null ? Visibility.Collapsed : Visibility.Visible; public DelegateCommand BuyCommand { get; } public string Name => ProductStack.Product.Name; public string Price => $"({ProductStack.Product.Price} .)"; public Visibility IsAmountVisible => BuyCommand == null ? Visibility.Collapsed : Visibility.Visible; public int Amount => ProductStack.Amount; }
      
      





最後に、MainViewVMのコンストラクター:



 public MainViewVM() { ... //  UserBuyings = new ObservableCollection<ProductVM>(_user.UserBuyings.Select(ub => new ProductVM(ub))); ((INotifyCollectionChanged)_user.UserBuyings).CollectionChanged += (s, a) => { if (a.NewItems?.Count == 1) UserBuyings.Add(new ProductVM(a.NewItems[0] as ProductStack)); if (a.OldItems?.Count == 1) UserBuyings.Remove(UserBuyings.First(ub => ub.ProductStack == a.OldItems[0])); }; }
      
      





モデルとユーザーのVM購入、およびモデルとユーザーのウォレットVMの同期はほぼ同じです。 さらに、金銭保管庫とマシン内の商品の場合も同じ同期が順番に行われます。 したがって、コードの重複を避けるために、次のような同期関数を作成します。



 private static void Watch<T, T2> (ReadOnlyObservableCollection<T> collToWatch, ObservableCollection<T2> collToUpdate, Func<T2, object> modelProperty) { ((INotifyCollectionChanged)collToWatch).CollectionChanged += (s, a) => { if (a.NewItems?.Count == 1) collToUpdate.Add((T2)Activator.CreateInstance(typeof(T2), (T) a.NewItems[0])); if (a.OldItems?.Count == 1) collToUpdate.Remove(collToUpdate.First(mv => modelProperty(mv) == a.OldItems[0])); }; }
      
      





そして、コンストラクタで次のように使用します。



 Watch(_user.UserWallet, UserWallet, um => um.MoneyStack); Watch(_user.UserBuyings, UserBuyings, ub => ub.ProductStack);
      
      





この関数は、テンプレート、デリゲート、およびアクティベーターを使用して、指定されたタイプのインスタンスを作成します。 この機能は、そのままでは、VMを保持する必要のある「単純さと平面性」から逸脱しています。 ただし、迷惑なタイプミスが非常に一般的であるコードの複製(特に、複製されたフラグメントに小さいながらも多数の変更を加える必要がある場合)には、このような逸脱が必要です。 さらに、そのような関数には、わかりやすいコメントを付ける必要があります。



さらに:連続して実装するMainViewVMクラスでは、オートマトンに関連するプロパティとコマンドは満たされていないままでした。 それらを理解しましょう、良いことは今より速くなります、なぜなら お金と製品モデルのために、私たちはすでに作成しました。 Userクラスの場合と同様に、Automataモデルクラスを作成し、その中に製品とお金の同じ2つのコレクションを作成します。 また、Creditプロパティも実装します。



 public class Automata { public Automata() { //  _automataBank = new ObservableCollection<MoneyStack> (Banknote.Banknotes.Select(b => new MoneyStack(b, 100))); AutomataBank = new ReadOnlyObservableCollection<MoneyStack>(_automataBank); //  _productsInAutomata = new ObservableCollection<ProductStack>(Product.Products.Select(p => new ProductStack(p, 100))); ProductsInAutomata = new ReadOnlyObservableCollection<ProductStack>(_productsInAutomata); } public ReadOnlyObservableCollection<MoneyStack> AutomataBank { get; } private readonly ObservableCollection<MoneyStack> _automataBank; public ReadOnlyObservableCollection<ProductStack> ProductsInAutomata { get; } private readonly ObservableCollection<ProductStack> _productsInAutomata; public int Credit { get; } }
      
      





したがって、MainViewVMクラスで、Automataクラスのプライベートフィールドを追加し、コレクションコンストラクターで初期化します。



 public class MainViewVM : BindableBase { public MainViewVM() { ... _automata = new Automata(); //  AutomataBank = new ObservableCollection<MoneyVM>(_automata.AutomataBank.Select(a => new MoneyVM(a))); Watch(_automata.AutomataBank, AutomataBank, a => a.MoneyStack); //  ProductsInAutomata = new ObservableCollection<ProductVM>(_automata.ProductsInAutomata.Select(ap => new ProductVM(ap))); Watch(_automata.ProductsInAutomata, ProductsInAutomata, p => p.ProductStack); } ... private Automata _automata; }
      
      





モデルの振る舞い



今残っているのは、現金を預け入れ、買い、変更を要求するときのモデルの動作を認識することです。



ご覧のとおり、これまでのところ、実際には設計上の決定を行っていません。 私たちのプログラムは、開発したユーザーインターフェイスとMVVMのニーズから必然的に成長しました。 インターフェイスの開発では、創造的な努力を払い、ほぼ自動的にVMを取得しました。 同時に、VMとモデルの間の接点が現れ、モデルのさらなる構築のための参照点になりました。 このアプローチ(最初に表示)は、開発されたモジュールのスケッチまたはフレームがいわば、広いストロークで作成された後、実際のプロジェクトに適用できます。



現在、開発を継続するには、ユーザーとマシンを1つのエンティティに結合する必要があります。 この関連付けは通常、この予備的なスケッチによって決定されます。 この場合、PurchaseManagerなどのクラスオブジェクト内の任意のユーザーとマシンの固定接続を作成します。したがって、この特定のクラスのオブジェクトに対するモデルの振る舞いに対する、まだ実装されていない要求に対処します。



なぜUserクラスとAutomataクラスにとどまらないのですか?原則として、もちろんできます。見てください:ユーザーから一定の金額を受け取り、この金額を機械に預ける能力が必要です。VMでこのような操作を実行することはできません。これはモデルでのみ有効です。すなわちこの操作は、UserクラスまたはAutomataクラスで実行する必要があります。単一の責任の分離の原則によれば、この責任は第3クラスに割り当てられ、相互作用を実行します。そのため、UserとAutomatを個別に作成する代わりに、PurchaseManagerクラスを作成し、MainViewVM.csを編集してこのクラスを使用します。



 PurchaseManager.cs: public class PurchaseManager { public User User { get; } = new User(); public Automata Automata { get; } = new Automata(); } MainView.cs: public class MainViewVM : BindableBase { private PurchaseManager _manager; public MainViewVM() { _manager = new PurchaseManager(); _user = _manager.User; _automata = _manager.Automata; ... } ... }
      
      





, DelegateCommand InsertCommand MoneyVM. VM . DelegateCommand VM. (PurchaseManager), , — , , . :



MoneyVM:



 public MoneyVM(MoneyStack moneyStack, PurchaseManager manager = null) { MoneyStack = moneyStack; if (manager != null) //  Null,   ,    DelegateCommand InsertCommand = new DelegateCommand(()=>{ manager.InsertMoney(MoneyStack.Banknote); }); }
      
      





Watch, MainViewVM. , . ( , ).



InsertMoney. , , , . , — internal.



PurchaseManager.cs:



 public void InsertMoney(Banknote banknote) { if (User.GetBanknote(banknote)) //     , Automata.InsertBanknote(banknote); //     }
      
      





User.cs:



 //  MoneyStack  ,       / // false    internal bool GetBanknote(Banknote banknote) { if(_userWallet.FirstOrDefault(ms => ms.Banknote.Equals(banknote))?.PullOne() ?? false) { RaisePropertyChanged(nameof(UserSumm)); //   ! return true; } return false; } //   public int UserSumm { get { return _userWallet.Select(b => b.Banknote.Nominal * b.Amount).Sum(); } }
      
      





MoneyStack.cs:



 internal bool PullOne() { if (Amount > 0) { --Amount; return true; } return false; }
      
      





Automata.cs:



 //       internal void InsertBanknote(Banknote banknote) { _automataBank.First(ms => ms.Banknote.Equals(banknote)).PushOne(); Credit += banknote.Nominal; } // private int credit; public int Credit { get { return credit; } set { SetProperty(ref credit, value); }}
      
      





MoneyStack.cs:



 internal void PushOne() => ++Amount;
      
      







INotifyPropertyChanged View.



MoneyStack BindableBase Amount :



 public class MoneyStack : BindableBase { ... private int _amount; public int Amount { get { return _amount; } set { SetProperty(ref _amount, value); } } }
      
      





MoneyVM BindableBase — MoneyVM:



 ... moneyStack.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Amount)); };
      
      





UserSumm Credit MainViewVM:



_user.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(UserSumm)); };

_automata.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Credit)); };







/- ! . - . , . ProductVM .



ProductVM:



 public ProductVM(ProductStack productStack, PurchaseManager manager = null) { ProductStack = productStack; productStack.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Amount)); }; if (manager != null) BuyCommand = new DelegateCommand(() => { manager.BuyProduct(ProductStack.Product); }); }
      
      





PurchaseManager.cs:



 public void BuyProduct(Product product) { if (Automata.BuyProduct(product)) User.AddProduct(product); }
      
      





Automata.cs:



 internal bool BuyProduct(Product product) { if(Credit >= product.Price && _productsInAutomata.First(p=>p.Product.Equals(product)).PullOne()) { Credit -= product.Price; return true; } return false; }
      
      





User.cs:



 internal void AddProduct(Product product) { var stack = _userBuyings.FirstOrDefault(b => b.Product == product); if (stack == null) _userBuyings.Add(new ProductStack(product, 1)); else stack.PushOne(); }
      
      





ProductStack.cs:



 public int Amount { get { return _amount; } set { SetProperty(ref _amount, value); } } internal bool PullOne() { if (Amount > 0) { --Amount; return true; } return false; } internal void PushOne() => ++Amount;
      
      





INotifyPropertyChanged View ProductVM:



 ... productStack.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Amount)); };
      
      







— , , — ( , ) .



 class PurchaseManager { ... public void GetChange() { IEnumerable<MoneyStack> change; if (Automata.GetChange(out change)) User.AppendMoney(change); } } // Automata internal bool GetChange(out IEnumerable<MoneyStack> change) { change = new List<MoneyStack>(); if (Credit == 0) return false; var creditToReturn = Credit; var toReturn = new List<MoneyStack>(); foreach (var ms in _automataBank.OrderByDescending(m => m.Banknote.Nominal)) { if (creditToReturn >= ms.Banknote.Nominal) { toReturn.Add(new MoneyStack(ms.Banknote, creditToReturn / ms.Banknote.Nominal)); creditToReturn -= (creditToReturn / ms.Banknote.Nominal) * ms.Banknote.Nominal; } } if (creditToReturn != 0) return false; //  ,    foreach (var ms in toReturn) // for (int i = 0; i < ms.Amount; ++i) //    _automataBank.First(m => Equals(m.Banknote, ms.Banknote)).PullOne(); change = toReturn; Credit = 0; return true; } // User internal void AppendMoney(IEnumerable<MoneyStack> change) { foreach (var ms in change) for(int i=0; i<ms.Amount;++i) UserWallet.First(m => Equals(m.Banknote.Nominal, ms.Banknote.Nominal)).PushOne(); RaisePropertyChanged(nameof(UserSumm)); }
      
      





それだけです : Vending Machine ( )



All Articles