Aeroフレームワークを介したコンテキストモデルパターン

コンテキストモデルパターン -MVVMMVPMVCアーキテクチャパターンの機能を組み合わせたアプリケーション設計コンセプトであり、かなり自由で進歩的な、適切に調整された一連のプロビジョニングに基づいています。 礎石は表現調停者文脈のオブジェクトそれらのコレクションであり、基本的な推薦は直接注射の原理です



Aeroフレームワークは、問題のパターンに一致するxaml指向のアプリケーションを作成するために必要なすべてのインフラストラクチャを含むC#言語のオープンライブラリです。 適切に使用することで、ソースコードは、初心者開発者にとっても前例のないほど簡潔で、高性能で、直感的です。



パターンのアイデアに従うことで、アプリケーションのナビゲーションとローカリゼーションからデータ検証メカニズムの実装まで、多くの日常的なタスクを美しく自然に解決し、インターフェイスと論理ビューモデルの視覚的状態を維持できます。 しかし、最も重要なことは、明確で成功した非常にシンプルなアーキテクチャを構築するのに役立つことです。 さらに、開発の速度と品質が大幅に向上し、記述されたコードの量が数回削減され、 さまざまなプロジェクトで正常にテストされています



気配りのある患者の読者には、知識がgeneしみなく与えられます。



画像





コンテキスト基盤の原理( コンテキスト基盤の原則



ビューは、コンテキストオブジェクトの特定のインスタンスを視覚化する方法を記述するインターフェイスマークアップテンプレートです。 単語がテキスト、イントネーション、または状況(つまりコンテキスト)内で意味的な意味合いを獲得するように、ユーザーインターフェイスの視覚要素は、コンテキストオブジェクトと密接に関連する場合にのみ有益になります。言い換えると、ユーザーフレンドリーな形式でコンテキストに含まれる有用なデータを表示し始めます。



任意のタイプのインスタンスはコンテキストオブジェクトとして機能できます。コンテキストオブジェクトに含まれる情報の解釈方法を知る必要があるのは、ビュー自体だけです。 そして、アイデア自体でさえ、それ自体のコンテキストまたは別のアイデアになることができます。 さらに、表現はマルチコンテキストにすることができます。つまり、複数のソースオブジェクトからの情報を同時に使用できます。



つまり、エンドユーザーにとっては、プレゼンテーションコンテキストオブジェクトの束だけが意味をなします。 個別のプレゼンテーション自体は実用的な観点からは無意味であり、ユーザーはオブジェクトを非視覚的な形で知覚することはできません。 オブジェクトが表現と一致すると、 コンテキストの基本性のプロパティが取得され、表現は有益になります



CM-パターンには、次の基本的な規定が含まれます。



•1つまたは複数のコンテキストオブジェクト(およびその逆)を任意の表現に関連付けることができます(多対多の関係)。 表現とコンテキストオブジェクトの相互作用は、3種類のメディエーターを介して発生します。プロパティ、コマンド、コンテキストインジェクションのバインド。



•ビューモデルとモデルは異なる特性を持つエンティティですが、一般的なドメインモデリング機能を実行し、コンテキスト基本のプロパティを持っているため、1つの基本クラスをモデルとビューモデルの両方に適用することはまったく受け入れられます。



複雑な表現自体は、視覚的ツリーの要素である、より小さく基本的な表現で構成されています。 特別な種類のルートビューは、何らかの形式でのナビゲーションをサポートする画面です。



プレゼンテーションとコンテキストオブジェクトの相互作用は、 メディエーターを介して発生します。 これは、ビジネスロジックをインターフェイスロジックから簡単に分離できるため、非常に重要なポイントです。



その重要性は、多くの場合、表現の存続期間がコンテキストオブジェクト(ビューモデルなど)の存続期間よりも短いこと、および可能であれば、正しく実装されたメディエーターには、弱いサブスクリプションと、相互作用するオブジェクトへのリンクのみが含まれていることです。メモリリークから保護します。



メディエーターは、3つの主要な機能を実行します。

•プロパティバインディング( BindingStoreBinding

•バインディングコマンド( コンテキスト

•コンテキストインジェクション( ストア



xamlに関して、ビューには個別のマークアップページとコントロールテンプレートおよびデータテンプレートの両方が含まれます。 いずれにせよ、 複雑な表現自体は、より小さくより基本的な表現-視覚的なツリーの要素で構成されます



例を考えてみましょう:

[DataContract] // serialization attributes public class Person : ModelBase // DTO or ORM entity { [DataMember] public string FirstName { get; set; } [DataMember] public string LastName { get; set; } // Possible, contains Save hanges logic }
      
      





 public class PersonsViewModel : ViewModelBase { public ObservableCollection<Person> Persons { get; set; } // Load, Save, Add, Remove items logic }
      
      





 <ListBox ItemsSource={Binding Persons}> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding FirstName}"/> <TextBlock Text="{Binding LastName}"/> </StackPanel> </DataTemplate> <ListBox.ItemTemplate> </ListBox>
      
      







ここでは、 Personエンティティモデルのコレクションがインターフェイス要素に添付され、モデルデータがインターフェイスに直接表示されます。 別の状況:



 public class TextDocumentViewModel : ViewModelBase { public string FileName { get; set; } public string Text { get; set; } // Load, Save document logic }
      
      





 public class DocumentsViewModel : ViewModelBase { public ObservableCollection<TextDocumentViewModel> Documents { get; set; } // Open, Save, Create, Close documents logic }
      
      





 <TabControl ItemsSource={Binding Persons} ItemTemplate={Binding FileName, Converter={StaticResource TitleConverter}}> <TabControl.ContentTemplate> <DataTemplate> <StackPanel> <TextBox AcceptsTab="True" AcceptsReturn="True" Text="{Binding Text, Mode=TwoWay}"/> </StackPanel> </DataTemplate> <TabControl.ContentTemplate> </TabControl>
      
      





TextDocumentViewModelTextモデルとFileNameモデルのデータを集約します。 つまり 、実際にはモデルでもありますが、 ViewModelBaseクラスから継承されますが、どちらの場合もPersonTextDocumentViewModelDataTemplateのデータコンテキストオブジェクトです。



しかし、これは、通常想定されるよりもビューモデルとモデルの類似性がまだあることを示していないのでしょうか? ModelBaseとViewModelBaseを単一のEntity ContextObjectに一般化するとどうなりますか?



同様のオブジェクトが持つべきプロパティを見てみましょう。 ViewModelBaseは、原則として、 何らかの方法でINotifyPropertyChangedインターフェイスを実装して、プロパティ値の変更についてインターフェイス要素に通知します。 ModelBaseはこのインターフェイスを実装することもあり、通常はシリアル化をサポートします 。 この機能はすべてContextObjectクラスによって実装されるため、可能であれば、ビューモデルだけでなくモデル自体の基礎として使用することをお勧めします。



そのような推奨事項は一見奇妙に見えるかもしれませんが、深く考えると、すべてが非常に自然です。ビューモデルとモデルの実際の目的の違いにもかかわらず、それらはデータコンテキストのオブジェクト(コンテキストオブジェクト)であるため、共通の基礎を持っている可能性が高いからです。 さらに、ビューモデルに非定型のシリアル化メカニズムを使用すると、以下に示すように、広範囲の一般的なタスクを非常に美しく解決できます。



さらに、 CMパターンの基本は追加の推奨事項のセットであり、これについて説明します。



シンプルさがアーキテクチャを成功させる鍵



最初のセクションでは、素材への読者の関心を喚起し、基本についての入門概念を示すために、 Aero Frameworkライブラリ( アプリケーション例のソースコードWebサイト )を使用してアプリケーションを構築する一般的なスキームを示し、次の重要な問題のサークルを簡単に説明します。



直接注入の原理直接注入の原理

Exposableパターンを介した独立した注入

コマンド指向ナビゲーション

•動的ローカリゼーション( 動的ローカライズ

•コンテキストコマンドとトリガー( コンテキストコマンドとトリガー

•プロパティの通知と検証( プロパティの通知と検証

•「スマート」状態( スマート状態



インターネットバンキングの機能の一部、つまり、承認画面と、アカウントと銀行カードを使用したユーザー製品に関する情報を作成します。 CMパターンによれば、プロジェクトには一連のナビゲート可能なビュー(画面)とそれらのビューモデルが含まれています。 さらに、特定のビューモデルでは複数の画面があり、いくつかの画面は同時に複数のビューモデルで動作できます。



LoginViewModelProductsViewModelを作成し、 ContextObjectクラスとIExposableインターフェイスからそれらを継承します。また、ユーザーを承認するLoginView 、選択する製品のリストを含むProductsView 、選択した製品とそのアクションのセットに関する詳細情報を含むProductViewを作成します。



 [DataContract] public class LoginViewModel : ContextObject, IExposable { [DataMember] public string Name { get { return Get(() => Name); } set { Set(() => Name, value); } } public string Password { get { return Get(() => Name); } set { Set(() => Name, value); } } public UserData UserData { get { return Get(() => UserData); } set { Set(() => UserData, value); } } public virtual void Expose() { this[() => Name].PropertyChanged += (sender, args) => Context.Login.RaiseCanExecuteChanged(); this[() => Password].PropertyChanged += (sender, args) => Context.Login.RaiseCanExecuteChanged(); this[() => Name].Validation += () => Error = Name == null || Name.Length < 3 ? Unity.App.Localize("InvalidName") : null; this[() => Password].Validation += (sender, args) => Error = Name == null || Name.Length < 4 ? Unity.App.Localize("InvalidPassword") : null; this[Context.Login].CanExecute += (sender, args) => { args.CanExecute = string.IsNullOrEmpty(Error); // Error contains last validation fail message }; this[Context.Login].Executed += async (sender, args) => { try { UserData = await Bank.Current.GetUserData(); Navigator.GoTo(args.Parameter); } catch (Exception exception) { Error = Unity.App.Localize(exception.Message); } }; this[Context.Logout].Executed += (sender, args) => { UserData = null; Navigator.RedirectTo(args.Parameter); }; } }
      
      





 <!--LoginView.xaml--> <View DataContext="{Store Key=viewModels:LoginViewModel}"> <StackPanel> <TextBlock Text="{Localizing Key=Name}"/> <TextBox Text="{Binding Name, Mode=TwoWay}"/> <TextBlock Text="{Localizing Key=Password}"/> <PasswordBox Password="{Binding Password, Mode=TwoWay}"/> <Button Command="{Context Key=Login}" CommandParameter="{x:Type ProductsView}" Content="{Localizing Key=Login}"/> <TextBlock Text="{Binding Error}"/> </StackPanel> </View>
      
      





 public class ProductsViewModel : ContextObject, IExposable { public Product CurrentProduct { get { return Get(() => CurrentProduct); } set { Set(() => CurrentProduct, value); } } public ContextSet<Product> Products { get; set; } public virtual void Expose() { Products = new ContextSet<Product>(); this[() => Name].PropertyChanged += (sender, args) => Context.Login.RaiseCanExecuteChanged(); this[() => Password].PropertyChanged += (sender, args) => Context.Login.RaiseCanExecuteChanged(); this[() => CurrentProduct].PropertyChanged += (sender, args) => Context.Get("GoToProduct").RaiseCanExecuteChanged(); this[Context.Get("GoToProduct")].CanExecute += (sender, args) => args.CanExecute = CurrentProduct != null; this[Context.Get("GoToProduct")].Executed += (sender, args) => Navigator.GoTo(args.Parameter); this[Context.Refresh].Executed += async (sender, args) => { try { var products = await Bank.Current.GetProducts(); CurrentProduct = null; Products.Clean(); products.ForEach(p => Products.Add); } catch (Exception exception) { Error = Unity.App.Localize(exception.Message); } }; } }
      
      





 <!--ProductsView .xaml--> <View DataContext="{Store Key=viewModels:ProductsViewModel}"> <Attacher.ContextTriggers> <ContextTrigger EventName="Loaded" UseEventArgsAsCommandParameter="False" Command="{Context Key=Refresh, StoreKey=viewModels:ProductsViewModel}"/> </Attacher.ContextTriggers> <StackPanel> <StackPanel DataContext="{Store Key=viewModels:LoginViewModel}"> <TextBlock Text="{Localizing Key=UserName}"/> <TextBlock Text="{Binding Path=UserData.FullName}"/> <Button Command="{Context Key=Logout}" CommandParameter="{x:Type LoginView}" Content="{Localizing Key=Logout}"/> </StackPanel> <ListBox ItemsSource="{Binding Products}" SelectedItem="{Binding CurrentProduct, Mode=TwoWay}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Balance, StringFormat=D2}"/> <TextBlock Text="{Binding CurrencyCode}"/> <StackPanel Visibility="{StoreBinding StoreKey=viewModels:SettingsViewModel, Path=ShowDetails, Converter="{StaticResource TrueToVisibleConverter}"}"> <TextBlock Text="{Localizing Key=ExpireDate}"/> <TextBlock Text="{Binding ExpireDate}"/> </StackPanel> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Content="{Localizing Key=Next}" Command="{Context Key=GoToProduct}" CommandParameter="{x:Type ProductView}"/> <StackPanel> </View>
      
      





 <!--ProductView .xaml--> <View DataContext="{Store Key=viewModels:ProductsViewModel}"> <StackPanel DataContext="{Binding CurrentProduct}"> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Balance, StringFormat=D2}"/> <TextBlock Text="{Binding CurrencyCode}"/> <TextBlock Text="{Localizing Key=ExpireDate}"/> <TextBlock Text="{Binding ExpireDate}"/> <StackPanel DataContext="{Store Key=viewModels:NavigatorViewModel}"> <Button Content="{Localizing Key=MakePayment}" Command="{Context Key=Navigate}" CommandParameter="{x:Type PaymentView}"/> <Button Content="{Localizing Key=MakeTransfer}" Command="{Context Key=Navigate}" CommandParameter="{x:Type TransferView}"/> </StackPanel> </StackPanel> </View>
      
      





それでは、ここで重要なポイントに注目しましょう。



1) 直接注入の原理直接注入の原理



ビューモデルとビューのリンクは、 Binding / Markup Extensionsを使用してコンテキストオブジェクトを直接注入することにより、 xamlで直接行われます



 DataContext="{Store Key=viewModels:ProductsViewModel}" Visibility="{StoreBinding StoreKey=viewModels:SettingsViewModel, Path=ShowDetails, Converter="{StaticResource TrueToVisibleConverter}"}"
      
      





プラットフォーム( WindowsデスクトップWindows Phoneなど)に応じて、マークアップパーサーの実装の詳細によって構文が異なる場合がありますが、原則は同じです。



Windowsデスクトップでは、次の方法で記述できます。



 DataContext="{Store viewModels:ProductsViewModel}" DataContext="{Store Key=viewModels:ProductsViewModel}" Visibility="{StoreBinding Path=ShowDetails, KeyType=viewModels:SettingsViewModel, Converter="{StaticResource TrueToVisibleConverter}" Visibility="{StoreBinding ShowDetails, KeyType=viewModels:SettingsViewModel, Converter="{StaticResource TrueToVisibleConverter}"}" Visibility="{Binding ShowDetails, Source={Store viewModels:SettingsViewModel}, Converter="{StaticResource TrueToVisibleConverter}"}"
      
      





Windows Phoneでは、このビューのみが許可されます。



 DataContext="{m:Store Key=viewModels:ProductsViewModel}" Visibility="{m:StoreBinding StoreKey=viewModels:SettingsViewModel, Path=ShowDetails, Converter="{StaticResource TrueToVisibleConverter}"
      
      





C#コードの構文は非常に単純に見えます。



 var anyViewModel = Store.Get<AnyViewModel>();
      
      





インジェクションはDataContextコントロールだけでなく、 Bindingでも直接許可されることに注意してください! 2番目の方法は、リストデータを表示するとき、コンテキストが既にコレクションの要素であるとき、コントロールの異なるプロパティが異なるソースで動作する必要がある場合( マルチコンテキストの動作 )など、状況によって非常に役立ちます。



ProductsViewは 、異なるビューモデルに関連付けられた2つの論理部分で構成されているため、注意する価値があります。 最初の部分にはユーザーに関する情報が含まれており、 ログアウトを実行できます。2番目の部分には、ユーザー製品のリストが直接表示されます。 ほとんどの古典的な実装では、 LoginViewModelProductsViewModelに注入する必要がありますが、この場合、ビューモデルは互いにかなり独立しているため、ソリューションはより美しくなりました。



ただし、 直接注入原理は広くなっています。 たとえば、Bindable Dependancy Convertersを美しく実装することもできます。これについては、後で詳しく説明します。



2) Exposableパターンを介した独立した注入



重要なポイントは、 IExposableインターフェイスからの継承です。IExposableインターフェイスは、オブジェクトの遅延初期化を担当します。 これは、インスタンスへの参照が初期化前でも使用可能になることを意味します。これは、オブジェクトへのすべての参照が失われ、ガベージコレクターによって削除される前にリソースを解放できるDisposableパターン( IDisposabeインターフェイス)の使用とは正反対です。 このような遅延初期化には、コンストラクターを使用する従来の方法よりもいくつかの利点がありますが、2番目のオプションは除外されません。さらに、たとえばファイナライザーでDisposeメソッドを呼び出すなど、一緒に使用できます



遅延初期化を使用すると、相互注入を含む相互接続された2つのオブジェクトを初期化できる独立注入の原理を実装できます。



 public class ProductsViewModel : ContextObject, IExposable { public virtual void Expose() { var settingsViewModel = Store.Get<SettingsViewModel>(); this[Context.Get("AnyCommand")].Executed += (sender, args) => { // safe using of settingsViewModel } } }
      
      





 public class SettingsViewModel : ContextObject, IExposable { public virtual void Expose() { var productsViewModel = Store.Get<SettingsViewModel>(); this[Context.Get("AnyCommand")].Executed += (sender, args) => { // safe using of productsViewModel } } }
      
      





Exposableパターンの代わりにコンストラクターを使用する場合、ビューモデルの1つを作成しようとすると、再帰的なスタックオーバーフローが発生します。 もちろん、初期化( Exposeメソッドの実行)の瞬間に別の初期化されていないビューモデルを使用することは時々安全ではないという制限を覚えておく必要がありますが、実際にはそのような状況はまれであり、クラス設計の設計におけるエラーを示す可能性が最も高いです。



さらに継承が計画されている場合は、 Exposeメソッドを仮想修飾子とともに使用して、継承されたクラスでオーバーライドし、多態的な動作を提供できるようにする必要があります。



3) コマンド指向ナビゲーション



おそらくこれはかなり興味深い側面です。 異なるプラットフォームなど、複数のプロジェクトでビューモデルの同じビジネスロジックを再利用する可能性を提供するには、それらのUIクラスへの参照を削除する必要があります。 しかし、特定のビジネスルールの実装に応じて、プレゼンテーションを選択するメカニズムをどのように編成するのでしょうか? CM-パターンは、このための推奨事項を提供します - コマンドメカニズムを使用し、表現のタイプまたはuriをコマンドにパラメーターとして渡す必要があります



 <Button Content="{Localizing GoToPayment}" Command="{Context Key=GoTo}" CommandParameter="{x:Type PaymentView}">
      
      





 <Button Content="{Localizing GoToPayment}" Command="{Context Key=GoTo}" CommandParameter="/Views/PaymentView.xaml">
      
      





 public class AnyViewModel : ContextObject, IExposable { public virtual void Expose() { // this[Context.Get("GoTo")].CanExecute += (sender, args) => args.CanExecute = 'any condition'; this[Context.Get("GoTo")].Executed += (sender, args) => Navigator.GoTo(args.Parameter); } }
      
      





Navigatorクラスはこのパラメーターを受け取り、それに基づいて目的のビューに移動します。 直接注入メカニズムを使用すると、次の表現は必要なコンテキストオブジェクト(ビューモデル)に簡単に関連付けられます。したがって、どの表現もコンテキストオブジェクトにアクセスできるため、ナビゲーション中にサービスパラメータを直接転送する必要がなくなります。 特定の画面に切り替える機能は、チームのCanExecuteイベントによって規制されています。



すべてが非常にエレガントでシンプルになりました。 ただし、コマンドパラメータを取得したため、他のデータを送信することはできません。 しかし、美しい解決策があります。非常にプリミティブなSetクラスを使用します。



 public class Set : List<object> { }
      
      





これにより、さまざまな要素のコレクションをxamlで直接作成できます。 たとえば、リソースにフォームのレコードを配置することにより:



 <Set x:Key="ParameterSet"> <system:String>/Views/AnyView.xaml</system:String> <system:String>SecondParaneter</system:String> </Set>
      
      





コマンドにいくつかの引数を簡単に渡すことができます。



 <Button Content="{Localizing GoToAnyView}" Command="{Context Key=GoTo}" CommandParameter="{StaticResource ParameterSet}">
      
      







4)動的なローカリゼーション



ローカリゼーションは、バインディング/マークアップ拡張機能を使用して実装されます。これにより、アプリケーションの実行中にホット言語の変更を実装できます。 つまり、ローカライズされた文字列のソースは、キーによって必要な値が抽出される注入されたコンテキストオブジェクトです。



Windowsデスクトップの場合、非常に単純な構文が受け入れられます。



 Text="{Localizing Name}" Text="{Localizing Key=Name}"
      
      





Windows Phoneはもう少し複雑ですが、非常に基本的なものでもあります。



 Text="{m:Localizing Key=Name}"
      
      





C#コードでは、適切なタイミングでリソースマネージャーをインストールする必要があります(アプリケーションのロード時または言語の変更時)。



 Localizing.DefaultManager.Source = English.ResourceManager;
      
      





そして、次のようなローカライズされた文字列を取得できます。



 var localizedValue = Unity.App.Localize("KeyValue");
      
      







5) コンテキストコマンドとトリガー



古典的なデスクトップWPFでは、強力で便利なRouted Commandsの概念が詳細開発されましたが、技術的な違いなどにより他のプラットフォームではサポートされていないため、普遍的、構文的、イデオロギー的に類似した何かを作成できるかどうかという疑問が生じました同時にルーティング可能なコマンドと互換性があります。



その結果、 コンテキストコマンドのアイデアが生まれました。 上記のように、非常に使いやすく、さらに、 Routed Commandsと完全に互換性があります。



 <Button Content="{Localizing GoToPayment}" Command="{Context Key=GoTo}" CommandParameter="/Views/PaymentView.xaml"/>
      
      





 <Button Content="{Localizing New}" Command="New"/>
      
      





  public class AnyViewModel : ContextObject, IExposable { public virtual void Expose() { // Context Command this[Context.Get("GoTo")].CanExecute += (sender, args) => args.CanExecute = 'any condition'; this[Context.Get("GoTo")].Executed += (sender, args) => Navigator.GoTo(args.Parameter); // Routed Command this[ApplicationCommands.New].CanExecute += (sender, args) => args.CanExecute = 'any condition'; this[ApplicationCommands.New].Executed += (sender, args) => AddNewDocument(); } }
      
      





静的Contextクラスには、以前に宣言されたいくつかのコマンド名が含まれており、必要に応じて独自のコマンド名を追加できます。 この[Context.Make] とこの[Context.Get( "Make")]のエントリ互いに同等です。 コンテキストコマンドの実装は、メモリリークに関して安全であることに注意してください。状況によっては、コマンドのCanExecuteChangedイベント制御をサブスクライブすると、ガベージコレクションからインターフェイスが抑止される可能性があります。



コンテキストコマンドは、ビジュアルツリーを介してルーティングされません 、ビジュアル要素自体またはその祖先のDataContextプロパティで設定されている最も近いコンテキストオブジェクトで実行されます。 ただし、直接注入の原則によれば、コンテキストオブジェクトはチームメディエーター自体に直接インストールできるため、柔軟性と汎用性に優れています。



 <Button DataContext="{Store viewModels:FirstViewModel}" Command="{Context Key=Make}">
      
      





 <Button DataContext="{Store viewModels:FirstViewModel}" IsEnabled="{Binding CanMake}" Command="{Context Key=Make, StoreKey=viewModels:SecondViewModel}">
      
      





最初の場合、MakeコマンドはFirstViewModelによって実行され、2番目の場合はSecondViewModelによって実行されます。



また、コマンドのコンテキストトリガーに言及する必要があります。 多くの場合、特定の制御イベントが発生すると、たとえばページの読み込み時にビジネスデータを要求または更新する必要がある場合、いくつかのアクションまたは一連のアクションを実行する必要があります。 この機能はコマンドトリガーを使用して実装され、 ルーティングイベントだけでなく、すべてのタイプのイベントがサポートされます。



 <Window> <Attacher.ContextTriggers> <Set> <ContextTrigger EventName="Loaded" Command="{Context Refresh, StoreKey=viewModels:AppViewModel}"/> <ContextTrigger EventName="Closing" UseEventArgsAsCommandParameter="True" Command="{Context Exit, StoreKey=viewModels:AppViewModel}"/> </Set> </Attacher.ContextTriggers> ... </Window>
      
      





この例では、トリガーはビジュアルツリーの継承ではないという事実に注意することが重要です。そのため、コンテキストオブジェクトはコマンドmediator- StoreKey = viewModels:AppViewModelに直接注入する必要があります。 コマンドにパラメーターとしてイベント引数を渡すこともできます。 したがって、ウィンドウを閉じるとき、 Closingイベントは、 args.Cancel = trueを設定することによりアクションを取り消す可能性があります



6) プロパティの通知と検証



インターフェイスおよび他のプログラムオブジェクトにプロパティの変更について通知するために、INotifyPropertyChangedインターフェイスがあります。 Aeroフレームワークは、このような目的のための便利なメカニズムを多数提供します。 まず、ContextObjectクラスから継承する必要があります。その後、簡潔で便利な構文を使用できるようになります。



 public class LoginViewModel : ContextObject, IExposable { // Auto with manual notification public string Name { get { return Get(() => Name); } set { Set(() => Name, value); } } // Only manual notification public string Password { get; set; } public virtual void Expose() { this[() => Name].PropertyChanging += (sender, args) => { // do anythink }; this[() => Name].PropertyChanged += (sender, args) => { // do anythink }; } }
      
      





プロパティを宣言する最初の方法では、他のオブジェクトに変更を自動的に通知できますが、次のような多態的なメソッドRaisePropertyChangingおよびRaisePropertyChangedを使用して、そのような通知を手動で行うこともできます。



 RaisePropertyChanging(() => Password); RaisePropertyChanged(() => Password);
      
      





 RaisePropertyChanging("Name"); RaisePropertyChanged("Name");
      
      





this [()=> Name] .PropertyChangedという表記は、面倒なif-else構造を回避し、さらに、外部からプロパティを変更するために簡単にサブスクライブします。



 var loginViewModel = Store.Get<LoginViewModel>(); loginViewModel[() => Name].PropertyChanging += (sender, args) => { // do anythink };
      
      





イベントにサブスクライブすると、サードパーティのリスニングオブジェクトがガベージコレクションから保持されるため、イベントから時間どおりにサブスクライブを解除するか、弱いサブスクリプションメカニズムを使用して、状況によってはメモリリークを回避する必要があります。



プロパティ値の検証も、このアプローチを使用してIDataErrorInfoインターフェイスを実装すると便利です。



 this[() => Name].Validation += (sender, args) => Error = Name == null || Name.Length < 3 ? Unity.App.Localize("InvalidName") : null; this[() => FontSize].Validation += () => 4.0 < FontSize && FontSize < 128.0 ? null : Unity.App.Localize("InvalidFontSize");
      
      







7)「スマート」状態( スマート状態



今、非常に珍しいですが、同時に状態を維持するための便利なメカニズムになります。 Aeroフレームワークを使用すると、この種の問題を解決するために、非常にエレガントで卓越した簡潔な動きを実現できます。



最初に、例のビューモデルはDataContract属性でマークされ、一部のプロパティはDataMember属性でマークされていることがあります 。これにより、シリアル化および逆シリアル化メカニズムを使用して論理状態を保存および復元できます。



このために必要なことは、アプリケーションの起動時に必要に応じてフレームワークを初期化することだけです。



 Unity.AppStorage = new AppStorage(); Unity.App = new AppAssistent();
      
      





デフォルトでは、シリアル化はファイルで行われますが、独自の実装を簡単に作成し、シリアル化されたオブジェクトをデータベースなどに保存できます。これを行うには、Unity.IApplicationインターフェイスから継承する必要がありますAppStorageはデフォルトで実装されています)。Unity.IApplicationAppAssistentインターフェースについては、シリアル化中の文化的設定に必要であり、ほとんどの場合、標準実装に制限できます。



シリアル化をサポートするオブジェクトの状態を保存するには、添付のSnapshotメソッドを呼び出すか、オブジェクトが共通コンテナーにある場合はStore.Snapshot呼び出しを使用します。



論理的な状態を維持することを考え出しましたが、多くの場合、ウィンドウのサイズと位置、コントロールの状態、その他のパラメーターなど、ストレージとビジュアルが必要です。CM-パターンは、非標準ですが非常に便利なソリューションを提供します。このようなパラメーターをコンテキストオブジェクト(ビューモデル)に格納し、シリアル化のための個別のプロパティとしてではなく、暗黙的に、キーが「仮想」プロパティの名前である辞書として格納するとどうなりますか?



この概念に基づいて、スマートプロパティのアイデアが生まれましたスマートプロパティの値は、ディクショナリのように、キー名でインデクサーを介してアクセスできる必要があります。クラシックgetまたはsetはオプションであり、存在しない場合があります。この機能はSmartObjectクラスに実装されています。ContextObjectの継承元である、展開します。



デスクトップ版で書くだけです:



 public class AppViewModel : SmartObject // ContextObject {}
      
      





 <Window x:Class="Sparrow.Views.AppView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:viewModels="clr-namespace:Sparrow.ViewModels" DataContext="{Store Key=viewModels:AppViewModel}" WindowStyle="{ViewModel DefaultValue=SingleBorderWindow}" ResizeMode="{Binding '[ResizeMode, CanResizeWithGrip]', Mode=TwoWay}" Height="{Binding '[Height, 600]', Mode=TwoWay}" Width="{ViewModel DefaultValue=800}" Left="{ViewModel DefaultValue=NaN}" Top="{Binding '[Top, NaN]', Mode=TwoWay}" Title="{ViewModel DefaultValue='Sparrow'}" Icon="/Sparrow.png" ShowActivated="True" Name="This"/>
      
      





! , . - - .



Window Phone , -, , smart - , , Indexer ViewModel . , :

 Background="{m:ViewModel Index=UseTable, FinalConverter={StaticResource BackgroundConverter}, Mode=OneWay, StoreKey=viewModels:SettingsViewModel, DefaultValue=True}"
      
      





 SelectedIndex="{m:ViewModel Index=SelectedIndex, StoreKey=viewModels:AppViewModel}"
      
      







, Indexer , , , . - , , . Aero Framework «» . , , SelectedIndex .

 <TabControl Grid.Row="2" Name="TabControl" MouseMove="UIElement_OnMouseMove" ItemsSource="{Binding Documents}" SelectedItem="{Binding Document}" SelectedIndex="{Binding '[DocumentIndex, 0, True].Value', Mode=TwoWay}">
      
      









 SelectedIndex="{Binding '[DocumentIndex, 0]', Mode=TwoWay}"
      
      





テストアプリケーションのすべてのタブを閉じて、Visual Studioでデバッグモードでメインウィンドウを移動するとVisual Studioの「出力」でトレースするインデックスプロパティの更新ごとにTabControlが例外を生成するため、スローダウンが発生します。このような状況はまれであり、ルールの例外である可能性が高くなりますが、分離を使用すると簡単に問題を解決できます。ポリモーフィズムメカニズムのおかげで、インデクサーも使用するIDataErrorInfoインターフェイスの実装によるプロパティ値の検証は、スマートステートの概念に非常にエレガントに適合します。







まとめ



上記の情報はすでにAero Frameworkライブラリを使用してCMパターンに対応するアプリケーションのプログラミングを開始するのに十分です。他に何を知る必要がありますか?まず、さまざまなシリアライザーの動作のニュアンスを把握しておくと便利です。AeroフレームワークはデフォルトDataContractJsonSerializerを使用しますが、必要に応じて別のものを使用できます。このコントラクトシリアライザーは、オブジェクトを逆シリアル化するときにコンストラクターを呼び出さず、既定のフィールドを初期化しないことを覚えておくことが重要です!











 // Incorrect !!! [DataContract] public class AnyViewModel: ContextObject { private AnyType Field1 = new AnyType(); private AnyType Field2; public AnyViewModel() { Field2 = new AnyType(); } }
      
      





 // Correct [DataContract] public class AnyViewModel : ContextObject { private AnyType Field1; private AnyType Field2; public AnyViewModel() { Initialize(); } // Called by serializer [OnDeserialized] // [OnDeserializing] public new void Initialize(StreamingContext context = default(StreamingContext)) { Field1 = new AnyType(); Field2 = new AnyType(); } }
      
      





 // Correct (recomended) [DataContract] public class AnyViewModel : ContextObject, IExposable { private AnyType Field1; private AnyType Field2; public virtual void Expose() { Field2 = new AnyType(); Field2 = new AnyType(); } }
      
      







また、KnownTypesコレクションのシリアライザーになじみのない型を追加する必要がある場合もあります。

 ContractFactory.KnownTypes.Add(typeof(SolidColorBrush)); //ContractFactory.KnownTypes.Add(typeof(MatrixTransform)); //ContractFactory.KnownTypes.Add(typeof(Transform)); ContractFactory.KnownTypes.Add(typeof(TextViewModel));
      
      







•拡張機能のバインド xaml-プラットフォームの



一部の実装では、同じ名前のクラスから継承することにより、独自のマークアップ拡張機能を作成できます。しかし、そのような機会はどこにも存在しません。それでも、ほとんどの場合、Bindingクラスからの継承は許可されています。



 using System; using System.Globalization; using System.Windows.Data; namespace Aero.Markup.Base { public abstract class BindingExtension : Binding, IValueConverter { protected BindingExtension() { Source = Converter = this; } protected BindingExtension(object source) // set Source to null for using DataContext { Source = source; Converter = this; } protected BindingExtension(RelativeSource relativeSource) { RelativeSource = relativeSource; Converter = this; } public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture); public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
      
      





MarkupExtension , , Binding, .



• Bindable Dependency Converters



, xaml , , , - . IValueConverter DependencyObject DependencyProperty、コンバータはビジュアルツリーの要素ではないため、ほとんどの場合、バインディングは機能しません!もちろん、さらに進んでハイブリッドコントローラーコンバーターを作成することもできますが、そのようなエキゾチックなソリューションは美しいとは言えず、その適用範囲は限られています。



しかし、前述のように、直接注入の原則は、StoreBindingDependency Converterに適用することを妨げるものではないため、救いになります



 <BooleanConverter x:Key="BindableConverter" OnTrue="Value1" OnFalse="Value2" OnNull="{StoreBinding StoreKey=viewModels: SettingsViewModel, Path=AnyValue3Path}"/>
      
      





Switch Converter



大規模なプロジェクトでは、多くの場合、switch文の動作に非常に類似したロジックを持つ同じタイプの多くの異なるタイプのコンバータークラスを作成する必要がありますしかし実際には、多くの場合、ユニバーサルスイッチコンバーターの使用に制限することができます。



 <SwitchConverter Default="ResultValue0" x:Key="ValueConverter1"> <Case Key="KeyValue1" Value="ResultValue1"/> <Case Key="KeyValue2" Value="ResultValue2"/> </SwitchConverter>
      
      





さらに、このコンバーターのプロパティ(Case構造を含む)はDependencyです。つまり、StoreBindingを使用したバインディングに使用できますさらに、キーがオブジェクト自体の値ではなくタイプである場合タイプモードがサポートされます



 <SwitchConverter TypeMode="True" Default="{StaticResource DefaultDataTemplate}" x:Key="InfoConverter"> <Case Type="local:Person" Value="{StaticResource PersonDataTemplate}"/> <Case Type="local:PersonGroup" Value="{StaticResource PersonGroupDataTemplate}"/> </SwitchConverter>
      
      





このようなコンバーターは、後者がサポートされていない場合でもDataTemplateSelectorとして簡単に適用できることがわかりました!SwitchConverterの汎用性により、膨大な数のケースをカバーできます。少し想像力をかければよいだけです。



•グローバルリソース:



コンバーターについて話し始めので、コンバーターで作業を整理する最適な方法を説明することは価値があります。まず、最も一般的なものを別のリソースディクショナリに配置する必要があります。



 <!--AppConverters .xaml--> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <BooleanConverter x:Key="NullToTrueConverter" OnNull="True" OnNotNull="False"/> <BooleanConverter x:Key="NullToFalseConverter" OnNull="False" OnNotNull="True"/> <BooleanConverter x:Key="NullToVisibleConverter" OnNull="Visible" OnNotNull="Collapsed"/> <BooleanConverter x:Key="NullToCollapsedConverter" OnNull="Collapsed" OnNotNull="Visible"/> <BooleanConverter x:Key="TrueToFalseConverter" OnTrue="False" OnFalse="True" OnNull="True"/> <BooleanConverter x:Key="FalseToTrueConverter" OnTrue="False" OnFalse="True" OnNull="False"/> <BooleanConverter x:Key="TrueToVisibleConverter" OnTrue="Visible" OnFalse="Collapsed" OnNull="Collapsed"/> <BooleanConverter x:Key="TrueToCollapsedConverter" OnTrue="Collapsed" OnFalse="Visible" OnNull="Visible"/> <BooleanConverter x:Key="FalseToVisibleConverter" OnTrue="Collapsed" OnFalse="Visible" OnNull="Collapsed"/> <BooleanConverter x:Key="FalseToCollapsedConverter" OnTrue="Visible" OnFalse="Collapsed" OnNull="Visible"/> <EqualsConverter x:Key="EqualsToCollapsedConverter" OnEqual="Collapsed" OnNotEqual="Visible"/> <EqualsConverter x:Key="EqualsToVisibleConverter" OnEqual="Visible" OnNotEqual="Collapsed"/> <EqualsConverter x:Key="EqualsToFalseConverter" OnEqual="False" OnNotEqual="True"/> <EqualsConverter x:Key="EqualsToTrueConverter" OnEqual="True" OnNotEqual="False"/> <AnyConverter x:Key="AnyToCollapsedConverter" OnAny="Collapsed" OnNotAny="Vsible"/> <AnyConverter x:Key="AnyToVisibleConverter" OnAny="Visible" OnNotAny="Collapsed"/> <AnyConverter x:Key="AnyToFalseConverter" OnAny="False" OnNotAny="True"/> <AnyConverter x:Key="AnyToTrueConverter" OnAny="True" OnNotAny="False"/> </ResourceDictionary>
      
      





次に、この辞書をApp.xamlのリソースに直接または間接的に含める必要があります。これにより、追加の手順を行わなくても、アプリケーションのほぼすべてのxaml-ファイルでメインコンバーターを使用できます。アプリケーションのグローバルリソースへのこのような追加は、多かれ少なかれ一般的なものに役立ちます。色、ブラシ、パターン、スタイル-たとえば、アプリケーションのテーマを変更するメカニズムを非常に簡単に実装するのに役立ちます。



 <Application x:Class="Sparrow.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Views/AppView.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <!--<ResourceDictionary Source="AppConverters.xaml"/>--> <ResourceDictionary Source="AppStore.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
      
      





•セット



前述のように、多くの場合に適用できるxamlのユニバーサルコレクションセットを使用すると便利な場合があります。場合によっては、「多階構造」を避け、一般的なポイントを作成し、コードをより正確にすることができます。



 <Set x:Key="EditMenuSet" x:Shared="False"> <MenuItem Header="{Localizing Undo}" Command="Undo"/> <MenuItem Header="{Localizing Redo}" Command="Redo"/> <Separator/> <MenuItem Header="{Localizing Cut}" Command="Cut"/> <MenuItem Header="{Localizing Copy}" Command="Copy"/> <MenuItem Header="{Localizing Paste}" Command="Paste"/> <MenuItem Header="{Localizing Delete}" Command="Delete"/> <Separator/> <MenuItem Header="{Localizing SelectAll}" Command="SelectAll"/> </Set> <MenuItem Header="{Localizing Edit}" ItemsSource="{StaticResource EditMenuSet}"/>
      
      







それだけですか?



そうではありません...主なコンポーネントは開発者のファンタジーです。それを開発します。探索、実験、構築!



PSインデックスプロパティの概念には、属性のない連想データモデルと共通点があるため、十分な強度と好奇心がある場合は、「思考という記事を読むのが理にかなっています。



All Articles