📜 ⬆️ ⬇️

Context Model Pattern via Aero Framework

Context Model Pattern is an application design concept that combines the features of MVVM , MVP , MVC architectural patterns and is based on a set of fairly free, progressive, well-coordinated positions. The cornerstones are representations , mediators , contextual objects and their collections , and the fundamental recommendation is the principle of direct injections .

Aero Framework is an open library in C # language, containing all the necessary infrastructure for creating xaml-oriented applications corresponding to the pattern in question. With its proper use, the source code is obtained unprecedentedly concise, high-performance and intuitive, even for novice developers.

Following the ideas of the pattern allows you to beautifully and naturally solve a lot of routine tasks from navigation and localization in the application to the implementation of data validation mechanisms, preservation of the visual state of the interface and logical view models. But the most important thing is helping to build a clear, successful and very simple architecture. Moreover, the speed and quality of development significantly increase, and the amount of written code is reduced several times, which has been successfully tested on various projects .
')
An attentive and patient reader will be generously rewarded by knowledge.

image


Context Context Principle

Views are interface markup templates that describe how to render specific instances of context objects. Just as words acquire a semantic coloring within a text, intonation or situation (that is, context), the visual elements of the user interface become informative only in close connection with the context object, in other words, they begin to display useful data contained in the context .

An instance of any type can act as a context object, only the representation itself must know how to interpret the information contained in the context object. And even the representation itself can be the context for itself or another representation. Moreover, representations can be multi-contextual, that is, they can simultaneously use information from several source objects.

In other words, for the end user, only a bundle representation-context object makes sense. A separate representation in itself is meaningless from a practical point of view, and the user is not able to perceive an object in a non-visual form. When an object is put into correspondence with a representation, it acquires the property of contextual fundamentality , and the representation becomes informative .

CM- pattern includes the following basic provisions:

• one or several context objects can be associated with any view and vice versa (many to many). Any interactions of representations and context objects occur through three types of mediators: property bindings, commands, and context injection;

• View models and models are entities that have different specifics, but perform common domain modeling functions and have the property of contextual fundamentality; therefore, applying the same base class to both the model and the view model becomes quite acceptable.

Complex representations themselves consist of smaller and elementary representations - elements of the visual tree. Root views are special types - screens that support navigation in one form or another.

Any interaction of a view with a context object occurs through mediators . This is an extremely important point, because it makes it easy to separate business logic from interface logic.

Its significance lies in the fact that often the lifetime of representations is shorter than the lifetime of context objects (for example, model view), and a correctly implemented mediator, if possible, contains only weak subscriptions and links to interacting objects, which does not prevent the view from garbage collection and protects against memory leaks.

Mediators perform three main functions:
• binding properties ( Binding , StoreBinding )
• command binding ( Context )
• context injection ( Store )

With regard to xaml , views include both individual markup pages, Control Controls and Data Templates . In any case, complex representations themselves consist of smaller and elementary representations - elements of the visual tree .

Consider an example:
[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> 


Here, the collection of entity entities of Person is bound to an element of the interface and the data of the model is directly displayed on the interface. Other situation:

 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> 

Now TextDocumentViewModel aggregates in itself the data models Text and FileName , that is, in fact, is also a model, although it is inherited from the ViewModelBase class, but in both cases Person and TextDocumentViewModel are data context objects for the DataTemplate .

But does this not indicate that there may still be more common features between the view model and the model than is usually assumed? What if ModelBase and ViewModelBase generalize to a single ContextObject entity?

Let's see what properties such an object should have. ViewModelBase , as a rule, in one way or another implements the INotifyPropertyChanged interface for notifying interface elements about changes in property values. ModelBase can also sometimes implement this interface and usually supports serialization . All this functionality is implemented by the ContextObject class, therefore, whenever possible, it is recommended to use it as a basis not only for the view models, but also for the models themselves.

At first glance, such recommendations may seem strange, but if you think about it more deeply, then everything is very natural, because despite the difference in practical use of the view models and models, they are data context objects (contextual objects), so they may well have a common basis . Moreover, using an atypical serialization mechanism for view models allows you to very nicely solve a wide range of common tasks, as will be shown below.

In addition, the basis of the CM- pattern is a set of additional recommendations, which are now discussed.

Simplicity - the key to good architecture

In the first section, in order to awaken the reader’s interest in the material and to give an introductory concept of the basic points, the general scheme of building an application using the Aero Framework library ( source codes with an example of the application , website ) will be demonstrated and the following range of important questions will be briefly highlighted:

• Direct Injections Principle
• Independent Injections via Exposable Pattern
• command-oriented navigation ( Command-Oriented Navigation )
• dynamic localization ( Dynamic Localizing )
• context commands and triggers ( Context Commands & Triggers )
• notification and validation of properties ( Property Notification & Validation )
• smart state

Let it be the task - to make part of the functionality for Internet banking, namely: authorization screens and information about the user's products with accounts and bank cards. According to the CM- pattern, the project contains a set of navigated views (screens) and a number of twist models for them, and, as for a particular twist model there can be several screens, and some screens can simultaneously work with several twist models .

Create LoginViewModel and ProductsViewModel , inheriting them from the ContextObject class and the IExposable interface, and LoginView to authorize the user, ProductsView with a list of products to select and ProductView with detailed information about the selected product and a set of actions on it.

 [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> 

So now pay attention to the key points.

1) Direct Injections Principle

Linking view-models with views occurs directly in xaml by directly injecting context objects using the Binding / Markup Extensions extensions .

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

Depending on the platform, for example, Windows Desktop or Windows Phone , the syntax may differ due to implementation details of the markup parser, but the principle remains the same.

On Windows Desktop, you can write in the following ways:

 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 only allows this:

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

The syntax of the C # code looks extremely simple:

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

Note that injection is valid not only in the DataContext control, but also directly into the binding ( Binding )! The second method helps a lot in some situations, for example, when displaying list data, when the context is already an element of the collection, and also in cases where different properties of the control must work with different sources ( multi-contextual behavior ).

It is worth paying attention to ProductsView , because it consists of two logical parts associated with different view-models. The first part carries information about the user and allows you to perform Logout , and the second directly displays a list of user products. In most classical implementations, LoginViewModel would have to be injected into ProductsViewModel , however, in our case, the solution turned out to be more beautiful, because the view models retained considerable independence from each other.

However, the principle of direct injections is broader. For example, it also allows you to beautifully implement Bindable Dependancy Converters, which will be described in detail below.

2) Independent Injections via Exposable Pattern

The important point is to inherit from the IExposable interface, which is responsible for the delayed initialization of the object. This means that the reference to the instance becomes available even before initialization, which is diametrically opposed to the use of the Disposable pattern ( IDisposabe interface), which allows you to release resources before all references to the object are lost and the garbage collector eliminates it. Such a lazy initialization has several advantages over the classical one with the help of the constructor, however, the second option is not excluded, moreover, they can be used together, as for example, in the finalizer, call the Dispose method.

With the help of lazy initialization, it is possible to implement the principle of independent injections , which allows us to initialize two interrelated objects containing reciprocal links to each other.

 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 } } } 

In the case of using a constructor instead of an Exposable pattern, an attempt to create one of the view models would cause a recursive stack overflow. Of course, one has to keep in mind the limitation that at the very moment of initialization (execution of the Expose method) it is sometimes unsafe to use another non-initialized view model, but in practice such situations are rare and rather indicate an error in the design of class design.

If further inheritance is planned, then the Expose method should be used with the virtual modifier so that it can be overridden in the inherited classes and provide polymorphic behavior.

3) Command-Oriented Navigation ( Command-Oriented Navigation )

Perhaps this is quite an interesting aspect. In order to provide the opportunity to reuse the same business logic with the view models in several projects, for example, for different platforms, you need to get rid of any references to UI classes. But how then to organize a mechanism for choosing a presentation depending on the implementation of certain business rules? CM- pattern gives its recommendation on this - you need to use the command mechanism, and the type of presentation or uri should be passed as a parameter to the command .

 <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); } } 

The Navigator class accepts this parameter and, based on it, navigates to the desired view. The following representation is easily linked with the necessary context object (I-view model) using the direct injection mechanism; therefore, any need to directly transfer any service parameters during navigation is lost, because any representation can get access to any context object. The ability to switch to one or another screen is governed by the command CanExecute event.

Everything turned out very elegant and simple, nothing superfluous. However, it can be argued that we have taken the command parameter and cannot pass on anything else. But again there is a beautiful solution: use the very primitive class Set :

 public class Set : List<object> { } 

It allows you to create collections of various elements directly in xaml . For example, by placing a record in the resources:

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

You can easily pass several arguments to the command:

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


4) Dynamic localization ( Dynamic Localizing )

Localization is also implemented through the use of binding / markup extensions, which allows for a hot change of language during the operation of the application. That is, the source of localized strings is the injected context object from which the necessary values ​​are extracted by key.

For Windows Desktop , the syntax is very simple:

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

For Windows Phone it is a bit more complicated, but also rather elementary:

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

In the C # code, you need to install the resource manager at the right time (when loading the application or changing the language):

 Localizing.DefaultManager.Source = English.ResourceManager; 

And you can get a localized string like this:

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


5) Context Commands & Triggers ( Context Commands & Triggers )

The classic desktop WPF has elaborated a powerful and convenient concept of routed commands ( Routed Commands ), but on other platforms it is not supported, partly due to technical differences, so the question arose whether it is possible to create something universal, syntactically and ideologically similar, and same time as routable commands.

As a result, the idea of contextual commands was born. As was shown above, it is extremely simple to use it and, moreover, it is perfectly compatible with 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(); } } 

The static Context class contains several previously declared command names, and if necessary, you can add your own. The entries this [Context.Make] and this [Context.Get ("Make")] are equivalent to each other. It is worth noting that the implementation of contextual commands is safe in terms of memory leaks, since in some situations the command's subscription to the CanExecuteChanged event can keep the interface from garbage collection, which is not immediately obvious.

Context commands are not routed through the visual tree, but are executed on the nearest context object, which is set in the DataContext property of the visual element itself or in its ancestors. But according to the principle of direct injections, the context object can be directly installed in the team mediator itself, which gives great flexibility and versatility.

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

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

In the first case, the Make command will be executed in FirstViewModel , and in the second, in SecondViewModel .

It is also necessary to mention the contextual trigger commands. Often, an action or series of actions must be performed when a certain control event occurs, for example, when a page is loaded, you need to request or update business data. This functionality is implemented using command triggers, and all event types are supported, not just Routed Events .

 <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> 

In this example, it is important to note that the triggers are not descendants of the visual tree, therefore the context object must be injected directly into the command mediator - StoreKey = viewModels: AppViewModel . It is also possible to pass as an argument parameter the event argument. Therefore, when closing the window, the Closing event, there is the possibility of canceling the action by setting args.Cancel = true .

6) Property Notification & Validation

To notify the interface and other program objects about changes in properties, there is an INotifyPropertyChanged interface. Aero Framework provides a number of convenient mechanisms for such purposes. First, it is necessary to inherit from the ContextObject class, after which the possibility of using a concise and convenient syntax will become available.

 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 }; } } 

The first way to declare properties allows you to automatically notify other objects about changes, but it is permissible to make such notification in manual mode using the polymorphic methods RaisePropertyChanging and RaisePropertyChanged , for example:

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

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

Writing this [() => Name] .PropertyChanged avoids cumbersome if-else constructions , and it is also easy to subscribe to changes in properties from the outside in a similar way:

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

Just remember that subscription to an event keeps a third-party listening object from garbage collection, so you need to unsubscribe from the event in time or use a weak subscription mechanism to avoid memory leaks in some situations.

Validation of property values ​​also looks comfortable using this approach and the implementation of the IDataErrorInfo interface.

 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) Smart State

Now we come to a very unusual, but at the same time useful mechanism for maintaining state. Aero Framework allows you to very elegantly and unmatchedly concisely solve problems of this kind.

First, you may have noticed that the view models in the examples are sometimes marked with the DataContract attribute, and some properties with the DataMember attribute , which allows you to use serialization and deserialization mechanisms to preserve and restore the logical state.

All you need to do to do this is to initialize the necessary framework during the launch of the application:

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

By default, serialization occurs in files, but you can easily create your own implementation and save serialized objects, for example, to a database. For this you need to inherit from the Unity.IApplication interface ( AppStorage is implemented by default ). As for the interface Unity.IApplication ( AppAssistent ), it is necessary for cultural settings during serialization and in most cases it can be limited to its standard implementation.

To save the state of any object that supports serialization, simply call the Snapshot attachment method, or use the Store.Snapshot call if the object is in the general container.

We have dealt with the preservation of the logical state, but it often becomes necessary to store and visually, for example, the size and position of the windows, the state of the controls and other parameters. CM- Pattern offers a non-standard, but incredibly convenient solution. What if you store such parameters in context objects (view models), but not as separate properties for serialization, but implicitly, as a dictionary, where the key is the name of the “imaginary” property?

Based on this concept, the idea of smart properties was born. The value of the smart property must be accessible through the indexer by the key name, as in a dictionary, and the classic get or set are optional and may be missing! This functionality is implemented in the SmartObject class, from which ContextObject inherits, extending it.

Just write in the desktop version:

 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"/> 

after which the size and position of the window will be automatically saved when you exit the application and are exactly restored at startup! Agree, this is an amazing conciseness for solving this kind of problem. - - .

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 , , TabControl , «Output» Visual Studio . , .

IDataErrorInfo , , -.

Results

The information presented is already enough to start programming applications that correspond to the CM pattern using the Aero Framework library .

What else do you need to know?

First, it is useful to have an idea of ​​the nuances in the work of various serializers. The Aero Framework uses the DataContractJsonSerializer by default , but you can use another one if necessary. It is important to remember that this contract serializer does not call a constructor when deserializing an object, and also does not perform initialization of fields by default!

 // 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(); } } 


It may also sometimes be necessary to add an unfamiliar serializer to the KnownTypes collection.
 ContractFactory.KnownTypes.Add(typeof(SolidColorBrush)); //ContractFactory.KnownTypes.Add(typeof(MatrixTransform)); //ContractFactory.KnownTypes.Add(typeof(Transform)); ContractFactory.KnownTypes.Add(typeof(TextViewModel)); 


• Binding Extensions

On some implementations of the xaml platform, it is possible to create your own markup extensions ( Markup Extensions ) by inheriting from the class of the same name. However, this possibility is not everywhere. But still, in most cases, inheritance from the Binding class is allowed.

 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 , , ! , -, , .

, , , StoreBinding Dependency Converter .

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

Switch converter

- switch . , Switch Converter:

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

Moreover, the properties of this converter (including Case designs) are Dependency , that is, available for binding using StoreBinding !In addition, type mode is supported when the key is not the object value itself, but the type:

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

It turns out that such a converter can easily be used as a DataTemplateSelector even where the latter is not supported!The versatility of SwitchConverter allows you to cover a huge number of cases, you just need to apply a little imagination to it.

• Global Resources

Since I started talking about converters, then it is worth telling how best to organize work with them. First of all, the most common need to bring in a separate resource dictionary:

 <!--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> 

After that, you must directly or indirectly merge this dictionary with resources in App.xaml, which will allow you to use the basic converters in almost any xaml- files of the application without additional actions. It is useful to make such an addition to the global resources of an application for any more or less common things: colors, brushes, patterns and styles — which makes it very easy to implement, for example, the theme change mechanisms in the application.

 <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> 

• Sets

As mentioned earlier, it may be useful to use the universal Set collection in xaml , which is applicable in many cases. Sometimes it allows you to avoid "multi-storey structures", make general points and make the code more accurate.

 <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 , , " ".

Source: https://habr.com/ru/post/251347/


All Articles