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.
Context Context PrincipleViews 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]
public class PersonsViewModel : ViewModelBase { public ObservableCollection<Person> Persons { get; set; }
<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; }
public class DocumentsViewModel : ViewModelBase { public ObservableCollection<TextDocumentViewModel> Documents { get; set; }
<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 architectureIn 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);
<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); } }; } }
<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>
<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 PrincipleLinking 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 PatternThe 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) => {
public class SettingsViewModel : ContextObject, IExposable { public virtual void Expose() { var productsViewModel = Store.Get<SettingsViewModel>(); this[Context.Get("AnyCommand")].Executed += (sender, args) => {
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() {
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() {
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 & ValidationTo 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 {
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) => {
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 StateNow 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
<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 , , -.
ResultsThe 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!
It may also sometimes be necessary to add an unfamiliar serializer to the KnownTypes collection. ContractFactory.KnownTypes.Add(typeof(SolidColorBrush));
• Binding ExtensionsOn 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)
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 ResourcesSince 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>
• SetsAs 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 , , "
".