📜 ⬆️ ⬇️

MVVM: a new look

Attention!
More recent and progressive materials on the MVVM pattern are presented in the article Context Model Pattern via Aero Framework and are discussed in detail in the next series of articles.

Foreword

Some time ago I started developing a free text editor with a beautiful interface and wide user-friendly functionality on the WPF platform. It was possible to solve a lot of technical problems, so I have gained some experience that I want to share with other people.
')
To business

Developers of WPF, Silverlight and WinPhone applications are familiar with the MVVM design pattern (Model - View - ViewModel). However, if you additionally apply a little more fantasy to it, then something more interesting may turn out, and I dare to assure you a little even that it is revolutionary.

Suppose we have a classic window (View) of a text editor with a menu, a toolbar and a status bar that can be hidden if desired. Our task is to preserve the position and size of the window, as well as the visual state of the elements when the application is closed, and then restore them.

The usual solution that immediately comes to mind is to add a number of additional properties to the view model for snapping (Top, Left, Width, Heigth, ShowToolBarTray, ShowStatusBar and others), and then saving their values, for example, to a file. But let's not hurry ... What if I tell you that you can create such a view model that implements the necessary functionality by default, so you don’t need ANY extra line of code to solve the problem?

Immediately I recommend downloading the sample application, which I made specifically for this article (link one or two ) , it will help you understand the main ideas and experience the beauty of the approach. Here I will give certain parts of the code that you should pay special attention to.

In WPF, property binding is often used, but it is also possible to bind to elements of an array, which is used quite rarely. But here it opens up new horizons for us. Let's try to consider the view model as a dictionary, where the key-index is the name of the property by which its value can be obtained.

But how do we better preserve these values? Let's try to serialize the view models! But? .. This is not a DTO object, and how can they be deserialized, because the constructor often needs to inject other parameters, and for deserialization you usually need a constructor without parameters? And you didn’t seem to be somewhat uncomfortable injecting into the constructor, for example, when adding or removing a parameter, unit tests broke, and they also needed to be edited, although the interface of the object under test remained essentially the same?

Therefore, we give up injections into the constructor, thankfully, there are other ways for such purposes, and mark the view model with the [DataContract] attribute, and the properties that need to be serialized with the [DataMember] attribute (these attributes greatly simplify serialization).

Now create a small class Store.

public static class Store { private static readonly Dictionary<Type, object> StoredItemsDictionary = new Dictionary<Type, object>(); public static TItem OfType<TItem>(params object[] args) where TItem : class { var itemType = typeof (TItem); if (StoredItemsDictionary.ContainsKey(itemType)) return (TItem) StoredItemsDictionary[itemType]; var hasDataContract = Attribute.IsDefined(itemType, typeof (DataContractAttribute)); var item = hasDataContract ? Serializer.DeserializeDataContract<TItem>() ?? (TItem) Activator.CreateInstance(itemType, args) : (TItem) Activator.CreateInstance(itemType, args); StoredItemsDictionary.Add(itemType, item); return (TItem) StoredItemsDictionary[itemType]; } public static void Snapshot() { StoredItemsDictionary .Where(p => Attribute.IsDefined(p.Key, typeof (DataContractAttribute))) .Select(p => p.Value).ToList() .ForEach(i => i.SerializeDataContract()); } } 


Everything is simple - only two methods. OfType returns us a static instance of an object of the required type, possibly deserializing it, and Snapshot takes a “snapshot” of the objects in the container, serializing them. In general, a snapshot can be called only once when the application is closed, for example, in the Exit handler of the Application class.

And write Json-serializer.

  public static class Serializer { public const string JsonExtension = ".json"; public static readonly List<Type> KnownTypes = new List<Type> { typeof (Type), typeof (Dictionary<string, string>), typeof (SolidColorBrush), typeof (MatrixTransform), }; public static void SerializeDataContract(this object item, string file = null, Type type = null) { try { type = type ?? item.GetType(); if (string.IsNullOrEmpty(file)) file = type.Name + JsonExtension; var serializer = new DataContractJsonSerializer(type, KnownTypes); using (var stream = File.Create(file)) { var currentCulture = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; serializer.WriteObject(stream, item); Thread.CurrentThread.CurrentCulture = currentCulture; } } catch (Exception exception) { Trace.WriteLine("Can not serialize json data contract"); Trace.WriteLine(exception.StackTrace); } } public static TItem DeserializeDataContract<TItem>(string file = null) { try { if (string.IsNullOrEmpty(file)) file = typeof (TItem).Name + JsonExtension; var serializer = new DataContractJsonSerializer(typeof (TItem), KnownTypes); using (var stream = File.OpenRead(file)) { var currentCulture = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; var item = (TItem) serializer.ReadObject(stream); Thread.CurrentThread.CurrentCulture = currentCulture; return item; } } catch { return default(TItem); } } } 


The base class for twisting models is also not difficult.

  [DataContract] public class ViewModelBase : PropertyNameProvider, INotifyPropertyChanging, INotifyPropertyChanged { protected Dictionary<string, object> Values = new Dictionary<string, object>(); private const string IndexerName = System.Windows.Data.Binding.IndexerName; /* "Item[]" */ public event PropertyChangingEventHandler PropertyChanging = (sender, args) => { }; public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { }; public object this[string key] { get { return Values.ContainsKey(key) ? Values[key] : null; } set { RaisePropertyChanging(IndexerName); if (Values.ContainsKey(key)) Values[key] = value; else Values.Add(key, value); RaisePropertyChanged(IndexerName); } } public object this[string key, object defaultValue] { get { if (Values.ContainsKey(key)) return Values[key]; Values.Add(key, defaultValue); return defaultValue; } set { this[key] = value; } } public void RaisePropertyChanging(string propertyName) { PropertyChanging(this, new PropertyChangingEventArgs(propertyName)); } public void RaisePropertyChanged(string propertyName) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } [OnDeserializing] private void Initialize(StreamingContext context = default(StreamingContext)) { if (PropertyChanging == null) PropertyChanging = (sender, args) => { }; if (PropertyChanged == null) PropertyChanged = (sender, args) => { }; if (Values == null) Values = new Dictionary<string, object>(); } } 


We will also inherit from a small class PropertyNameProvider, which will be useful to us later on for working with lambda expressions.

  [DataContract] public class PropertyNameProvider { public static string GetPropertyName<T>(Expression<Func<T>> expression) { var memberExpression = expression.Body as MemberExpression; var unaryExpression = expression.Body as UnaryExpression; if (unaryExpression != null) memberExpression = unaryExpression.Operand as MemberExpression; if (memberExpression == null || memberExpression.Member.MemberType != MemberTypes.Property) throw new Exception("Invalid lambda expression format."); return memberExpression.Member.Name; } } 


Well, at this stage, we implemented the ability to bind to the properties-indices. You can write the following form in xaml

Height = "{Binding '[Height, 600]', Mode = TwoWay}"

where the first parameter is the name of the property, and the second (optional) is its default value.

This approach is somewhat similar to the implementation of the standard IDataErrorInfo interface. Why don't we implement it too? Good idea, but let's not hurry, but take it into account ... Let's play with the redefinition of the indexer. Everyone remembers about ICommand, and in WPF there is still a cool mechanism for working RoutedCommands and CommandBindings. It would be great to write the implementation of commands in a view model in this way.

  this[ApplicationCommands.Save].CanExecute += (sender, args) => args.CanExecute = HasChanged; this[ApplicationCommands.New].CanExecute += (sender, args) => { args.CanExecute = !string.IsNullOrEmpty(FileName) || !string.IsNullOrEmpty(Text); }; this[ApplicationCommands.Help].Executed += (sender, args) => MessageBox.Show("Muse 2014"); this[ApplicationCommands.Open].Executed += (sender, args) => Open(); this[ApplicationCommands.Save].Executed += (sender, args) => Save(); this[ApplicationCommands.SaveAs].Executed += (sender, args) => SaveAs(); this[ApplicationCommands.Close].Executed += (sender, args) => Environment.Exit(0); this[ApplicationCommands.New].Executed += (sender, args) => { Text = string.Empty; FileName = null; HasChanged = false; }; 


Well, what a view model without automatic notification of properties and expressions? It should be anyway.

  public string Text { get { return Get(() => Text); } set { Set(() => Text, value); } } 


But what if ... Create a PropertyBinding like a CommandBinding and play again a little bit with the indexer?

  this[() => Text].PropertyChanged += (sender, args) => HasChanged = true; this[() => FontSize].Validation += () => 4.0 < FontSize && FontSize < 128.0 ? null : "Invalid font size"; 


Looks good, isn't it?

And, of course, our wonder-view model.

  [DataContract] public class ViewModel : ViewModelBase, IDataErrorInfo { public ViewModel() { Initialize(); } string IDataErrorInfo.this[string propertyName] { get { return PropertyBindings.ContainsKey(propertyName) ? PropertyBindings[propertyName].InvokeValidation() : null; } } public PropertyBinding this[Expression<Func<object>> expression] { get { var propertyName = GetPropertyName(expression); if (!PropertyBindings.ContainsKey(propertyName)) PropertyBindings.Add(propertyName, new PropertyBinding(propertyName)); return PropertyBindings[propertyName]; } } public CommandBinding this[ICommand command] { get { if (!CommandBindings.ContainsKey(command)) CommandBindings.Add(command, new CommandBinding(command)); return CommandBindings[command]; } } public string Error { get; protected set; } public Dictionary<ICommand, CommandBinding> CommandBindings { get; private set; } public Dictionary<string, PropertyBinding> PropertyBindings { get; private set; } public CancelEventHandler OnClosing = (o, e) => { }; public TProperty Get<TProperty>(Expression<Func<TProperty>> expression, TProperty defaultValue = default(TProperty)) { var propertyName = GetPropertyName(expression); if (!Values.ContainsKey(propertyName)) Values.Add(propertyName, defaultValue); return (TProperty) Values[propertyName]; } public void Set<TProperty>(Expression<Func<TProperty>> expression, TProperty value) { var propertyName = GetPropertyName(expression); RaisePropertyChanging(propertyName); if (!Values.ContainsKey(propertyName)) Values.Add(propertyName, value); else Values[propertyName] = value; RaisePropertyChanged(propertyName); } public void RaisePropertyChanging<TProperty>(Expression<Func<TProperty>> expression) { var propertyName = GetPropertyName(expression); RaisePropertyChanging(propertyName); } public void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> expression) { var propertyName = GetPropertyName(expression); RaisePropertyChanged(propertyName); } [OnDeserializing] private void Initialize(StreamingContext context = default(StreamingContext)) { CommandBindings = new Dictionary<ICommand, CommandBinding>(); PropertyBindings = new Dictionary<string, PropertyBinding>(); PropertyChanging += OnPropertyChanging; PropertyChanged += OnPropertyChanged; } private void OnPropertyChanging(object sender, PropertyChangingEventArgs e) { var propertyName = e.PropertyName; if (!PropertyBindings.ContainsKey(propertyName)) return; var binding = PropertyBindings[propertyName]; if (binding != null) binding.InvokePropertyChanging(sender, e); } private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { var propertyName = e.PropertyName; if (!PropertyBindings.ContainsKey(propertyName)) return; var binding = PropertyBindings[propertyName]; if (binding != null) binding.InvokePropertyChanged(sender, e); } } 


Now we are fully armed, but there is no limit to perfection. As a rule, a twist model is associated with its view (twist) in C # code, but how beautiful it would be to do this binding directly in xaml! Remember about our refusal of injections in the designer? Here he gives us the opportunity. Let's write a small markup extension *.

  public class StoreExtension : MarkupExtension { public StoreExtension(Type itemType) { ItemType = itemType; } [ConstructorArgument("ItemType")] public Type ItemType { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget)); var frameworkElement = service.TargetObject as FrameworkElement; var dependancyProperty = service.TargetProperty as DependencyProperty; var methodInfo = typeof(Store).GetMethod("OfType").MakeGenericMethod(ItemType); var item = methodInfo.Invoke(null, new object[] { new object[0] }); if (frameworkElement != null && dependancyProperty == FrameworkElement.DataContextProperty && item is ViewModel) { var viewModel = (ViewModel) item; frameworkElement.CommandBindings.AddRange(viewModel.CommandBindings.Values); var window = frameworkElement as Window; if (window != null) viewModel.OnClosing += (o, e) => { if (!e.Cancel) window.Close(); }; frameworkElement.Initialized += (sender, args) => frameworkElement.DataContext = viewModel; return null; } return item; } } 


Voila, you're done!

DataContext = "{Store viewModels: MainViewModel}"

I draw attention to the fact that during binding, the control changes not only the DataContext, but also the CommandBindings collection is filled with values ​​from the view model.

(* In order not to write prefixes like "{foundation: Store viewModels: MainViewModel}" before markup extensions, they should be implemented in a separate project and you need to write something like this in the AssemblyInfo.cs file
 [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation.Converters")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation.MarkupExtensions")] 
)

In a similar way, we will embellish the binding to the indices, which was discussed above.

  public class ViewModelExtension : MarkupExtension { private static readonly BooleanConverter BooleanToVisibilityConverter = new BooleanConverter { OnTrue = Visibility.Visible, OnFalse = Visibility.Collapsed, }; private FrameworkElement _targetObject; private DependencyProperty _targetProperty; public ViewModelExtension() { } public ViewModelExtension(string key) { Key = key; } public ViewModelExtension(string key, object defaultValue) { Key = key; DefaultValue = defaultValue; } public string Key { get; set; } public string StringFormat { get; set; } public string ElementName { get; set; } public object DefaultValue { get; set; } public object FallbackValue { get; set; } public object TargetNullValue { get; set; } public IValueConverter Converter { get; set; } public RelativeSource RelativeSource { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget)); _targetProperty = service.TargetProperty as DependencyProperty; _targetObject = service.TargetObject as FrameworkElement; if (_targetObject == null || _targetProperty == null) return this; var key = Key; if (_targetProperty == UIElement.VisibilityProperty && string.IsNullOrWhiteSpace(key)) key = string.Format("Show{0}", string.IsNullOrWhiteSpace(_targetObject.Name) ? _targetObject.Tag : _targetObject.Name); key = string.IsNullOrWhiteSpace(key) ? _targetProperty.Name : key; if (!string.IsNullOrWhiteSpace(StringFormat)) Key = string.Format(StringFormat, _targetObject.Tag); var index = DefaultValue == null ? key : key + "," + DefaultValue; var path = string.IsNullOrWhiteSpace(ElementName) && RelativeSource == null ? "[" + index + "]" : "DataContext[" + index + "]"; if (_targetProperty == UIElement.VisibilityProperty && Converter == null) Converter = BooleanToVisibilityConverter; var binding = new Binding(path) {Mode = BindingMode.TwoWay, Converter = Converter}; if (ElementName != null) binding.ElementName = ElementName; if (FallbackValue != null) binding.FallbackValue = FallbackValue; if (TargetNullValue != null) binding.TargetNullValue = TargetNullValue; if (RelativeSource != null) binding.RelativeSource = RelativeSource; _targetObject.SetBinding(_targetProperty, binding); return binding.ProvideValue(serviceProvider); } } 


You can write xaml like this:

Width = "{ViewModel DefaultValue = 800}"

Results

Perhaps enough, I presented a lot of information in a compressed form, so for a complete understanding it is better to get acquainted with the example of the project.

Summarizing all the above, we can distinguish the following advantages of the approach:
- clean, concise and structured code. Interface logic, weakly related to business logic, is encapsulated inside the base classes of the view model, while the specific implementation of the view model contains exactly the logic that is closely related to business rules;
- simplicity and versatility. Everything else, serialization allows very flexible configuration of the application interface using configuration files;
- convenient implementation of validation through the IDataErrorInfo interface.

Minuses:
- refusal of injections into the constructor (although this is not a mandatory requirement);
- some implicit decision for a person not familiar with it.

Having mastered this approach and having only a few basic classes at your disposal, you can write applications comfortably, quickly and efficiently with a rich interactive interface, while leaving the twist models clean and compact.

I really hope that the article will be useful for you! Thanks for attention!

PS I don’t know exactly how in Silverlight, but on the WinPhone-platform there are some limitations (there are no markup extensions, RoutedCommands and CommandBindings), but with a strong desire they can be circumvented. This is described in more detail in the WinPhone article : Ways to Perfection .

PPS As I said above, all the methods described are applied by me when creating a full-fledged text editor. Those who are interested in, what ultimately turned out for the creation, can find it on this link or backup . It seems to me that in programming and poetry there is a lot in common : just as a master of words is able to express with a few phrases what an ordinary person will take more than one paragraph, so an experienced programmer solves a complex problem with several lines of code.

Inspiration to you!

~~~~~~~~~~~~~~~~~~~~~~~~~
Comments:

- you can use any control as a view (View), so sometimes it is not necessary to select the view as a separate entity (new class), as well as add a property to another view model. Let me explain with an example:

 <Window DataContext={Store viewModels:MainViewModel}> … <!--<TextBlock DataContext={Store viewModels:DetailsViewModel} Text={Binding Name}/>--!> <TextBlock Text={Binding Name, Source={Store viewModels:DetailsViewModel}}/> … </Window> 


That is, we do not need to inject the DetailsViewModel into the MainViewModel, just to display the Name property somewhere on the interface, it is also not necessary, for example, to create the DetailsShortView. The project has fewer classes, and the structure remains clear.

- in the article I showed the basic principles, using which you can quickly and efficiently make a functional application. It is absolutely not necessary to use everything as it is, you have the right to improve, modify and fantasize! This is the development, success!

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


All Articles