Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Dynamic WPF MVVM View Model Event, Command and Property Generation

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
3 Jul 2014Ms-PL8 min read 26.7K   920   21  
Enables automatic view model generation of properties, look up values, command, event and model linkage

Introduction

While the Model View View Model (MVVM) pattern provides a number of advantages (separation of concerns and ease of testing) to XAML based applications (WPF, Silverlight and Windows RT) . There are number of frustrations in getting things to work - especially in the amount of code that must be generated to do simple things.

A lot of time is spent coding view models which simply mirror properties and 'wrap' the underlying model objects, implementing the INotifyPropertyChanged (INTP) interface and creating delegates to handle command actions and events the view generates.

The following example illustrates a simple 'wrapped' model object. The corresponding ViewModel takes a Model object and exposes its properties:

C#
public partial class CarType
{
 public Int32 CarTypeID {get;set;}
 public string CarTypeName {get;set;}
}

public class CarTypeViewModel : ViewModelBase
{
CarType wrapped;
public CarTypeViewModel (CarType carType)
 {
  wrapped = carType;
 }

 public CarTypeViewModel (CarType carType)
 {
    wrapped = carType;
 }

public string CarTypeName {get
         { return wrapped.CarTypeName; }
          set {if (value!=wrapped.CarTypeName)
            {
             wrapped.CarTypeName =value;
             RaisePropertyChange("CarTypeName");
            }
         }
           }
    }
}

So ideally, I wanted to simplify a number of these repetitive tasks and investigated various options by automatically generating the appropriate delegates, properties and events.

The end result, among other things, allows the automatic binding of commands, events and INotifyPropertyChanged events, while still maintaining strong separation of concerns and requiring no direct linking with the view.

The following example illustrates a view model that would get auto mapped properties, commands and events:

C#
//note no property change events fired or separate variable to store value
     public string FirstName {get;set;}
     public string LastName{ get; set; }

     // FullName shows combined first and last name properties.
 //The LinkToProperty attributes ensures related property is fired when changes are made
     [LinkToProperty("FirstName")][LinkToProperty("LastName")]
     public string FullName
     {
         get
         {
             return String.Format("{0} {1}", FirstName, LastName);
         }
     }

     // Demonstrates wiring view events - this wires to datagrid AddingNewItem event
     [LinkToEvent("AddingNewItemEvent")]
     private void DataGrid_AddingNewItem(AddingNewItemEventArgs e)
     {
     }

 //links to View's RollBackChangesCommand command
     [LinkToCommand("RollBackChangesCommand")]
     private void RollBackChanges()
     {
         this.RollBack();
     }

     /// Wire up all property change events through attribute.
     [PropertyChangedEvent]
     void person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
     {

         if (e.PropertyName != "Log" && sender is ICustomObjectProxyObjects)
         {
             SetPropertyValue("Log", e.PropertyName + " changed to:" +
             ((ICustomObjectProxyObjects)sender).GetPropertyValue(e.PropertyName).ToString() + "\r\n" + Log);
         }
     }

One solution would automatically map a views method and events against a corresponding view model - but this strongly coupled the view and view model. I also looked at the dynamic ExpandoObject, which looked promising but ended up not working well either.

The solution came in the form of ICustomType interface which was introduced in Silverlight 5 and WPF 4.5:

C#
public interface ICustomTypeProvider
{
    Type GetCustomType();
}

It looks deceptively simply - and rightly so. Both WPF 4.5 and Silverlight 5 will use this custom type if the view's DataContext implements the interface.

In order to implement, you need to replace the underlying property Type class implementation. When a view makes a request, the custom type 'intercepts' the request and deals with it accordingly. It is up to you how this is implemented. The power here is you have complete control over what is returned to the view. But you must also take care of any 'normal' properties exposed in the underlying class.

The article Binding to Dynamic Properties with ICustomTypeProvider by Alexandra Rusina describes this process and forms the basis of this solution.

The resulting ProxyTypeHelper solution is intended to help simplify the following tasks:

  • Auto generation of view model properties from underlying model
  • Auto creation of command and event delegates
  • Auto mapping lookup values/properties
  • Dirty flag on per field/property level
  • Storing of original value /rollback changes

It does this while maintaining separation of concerns - no dependencies added between the underlying models, views or view models. The included sample code provides examples on these tasks as well as demonstrating a dynamic view model.

Mapping View to View Model Properties and Dynamic Properties

One way the ProxyTypeHelper library maps models to view models is via the AssociatedModelattribute. The AssociatedModel attribute identifies the class to be automatically mapped. Note the class you are applying the attribute to must inherit from ProxyHelper. The following code associates the earlier CarType model class with the CarTypeViewModel:

C#
using ProxyHelper;
using Model;

namespace WPFEventInter.ViewModel
{
[AssociatedModel(typeof(CarType))]
public class CarTypeViewModel : ViewModelValidated<cartypeviewmodel>{}
}

In order for the ProxyTypeHelper to recognize view models using the AssociatedModel attribute, you must call the TypeStuff class static InitializeProxyTypes method before using. You only need to call this method once and it will search for all classes with the attribute. The App.xaml.cs is a good place to put it.

C#
TypeStuff.InitializeProxyTypes();

If model class properties implements ICollection (such as List<>), they will get automatically created as ObservableCollections in the view model - and corresponding model objects are created or deleted as required.

You can manually associate models to view models using the static AddProxyObject method - so you could have multiple associated models within a view:

C#
PersonViewModel.AddProxyObject(typeof(Person));

Alternatively, you can add individual properties to a class using the static AddProperty method, which requires the property name and type:

C#
PersonViewModel.AddProperty("Name", typeof(string));

You need to use the AddProperty or AddProxyObject before an instance of the class is created for a view. Any properties added after the object has been created are not seen by the view.

Any properties created in classes that inherit ProxyTypeHelper will automatically have INotifyPropertyChanged events fire upon changes:

C#
using ProxyHelper;

namespace WPFEventInter.ViewModel
  {
  public class ProductViewModel : ProxyTypeHelper<productviewmodel>
   {
     public string Description { get; set; }
      public double Price { get; set; }
    }
  }

Note you don't need to manually fire any property notifications as they are auto generated.

To create a property changed event, just add the PropertyChangedEvent attribute to the appropriate method. The event gets auto assigned to the object - any child objects are also auto assigned:

C#
[PropertyChangedEvent]
void person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
  if (e.PropertyName != "Log" && sender is ICustomObjectProxyObjects)
  {
   SetPropertyValue("Log", e.PropertyName + " changed to:" +
    ((ICustomObjectProxyObjects)sender).GetPropertyValue(e.PropertyName).ToString()
    + "\r\n" + Log);
  }
} 

The properties in the associated view models aren't directly visible from the CLR - but are from the view/form. Use GetPropertyValue/SetPropertyValue to get and set individual properties:

C#
GetPropertyValue(propertyName);
SetPropertyValue(propertyName,propertyValue);

Use SetValue sets all properties for a corresponding object:

C#
SetValue(Type proxyType, object value);

Where proxyType is the type of the underlying proxy object and value is value you are assigning:

C#
persons.SetValue(typeof(Person), person);

If you have properties that need to fire when other properties change, add the LinkToProperty attribute. In the following example, the FullName property changed will get fired if either FirstName or SurName properties are changed:

C#
[LinkToProperty("FirstName")] [LinkToProperty("SurName")]
public string FullName
{
  get
  {
   return String.Format("{0} {1}", GetPropertyValue<string>("FirstName"), GetPropertyValue<string>("SurName"));
  }
}

If you do not want the property changed event to fire, add the DoNotRaiseProperyChangedEvents attribute.

Auto Creation of Command and Event Delegates

Linking a view's command action to View model. While technically not difficult, the amount of code can add up if you have a particularly complex view, menu or button bar. Each action needs a corresponding delegate and public property to expose it to the view.

To assign a method to an event command, add the LinkToCommand attribute to a method:

C#
[LinkToCommand("RollBackChangesCommand")]
private void btnRollBackChanges()
{
}

The argument passed to LinkToCommand attribute must match the bound Command in the XAML control, as illustrated in the following button control:

C#
<Button Content="Rollback changes"
Command="{Binding RollBackChangesCommand}" Width="140" ></Button>

LinkToCommand creates a delegate linked to the method and a public property exposing the delegate to the view.

Events can be linked in a similar fashion but require a bit more work on the view side. Note that it can be argued that passing events to the view model tightly couples the view and view model, or at least the underlying controls. From a practical perspective, there is often no other solution than putting the events in the view model, especially if the view model is extensively reused.

You need to assign EventToCommandBehavior behaviour for each control in your view you wish to fire the events. This is not a standard XAML feature. Some frameworks and control libraries such as MVVM Light include this behavior.

An implementation is included as part of the project - you may need to change this code to work with other frameworks. In the following snippet, the DataGrid's AddingNewItem event is bound to the AddingNewItemEvent view model command:

C#
<i:Interaction.Behaviors>
<rmwpfi:EventToCommandBehavior Command="{Binding AddingNewItemEvent}" Event="AddingNewItem" PassArguments="True" />
</i:Interaction.Behaviors>

In your view model, you would add the method. Note the event arguments must be appropriate for the event being fired. The easiest way to determine this is to create the event in the view's code behind and copy it to the view model. The following snippet would create delegates for the previous example:

C#
[LinkToEvent("AddingNewItemEvent")]
private void DataGrid_AddingNewItem(AddingNewItemEventArgs e)
 {
 }

Auto Mapping Lookup Values

Lookup values can also be auto generated by storing lists of values against types:

C#
TypeStuff.SetLookupData("BrandsLookup", typeof(Brand), carsContext.Brands.ToList());
TypeStuff.SetLookupData("ColoursLookup", typeof(Colour), carsContext.Colours.ToList());

From the above example, any view model based on a model object that many to one entity will get the property auto created. So in the enclosed example the Cars ViewModel will get the ColoursLookup property auto created.

Tracking Dirty Properties and Original Values

The ability to determine if any object properties have changed is a common view model requirement. The simplest way is to set a boolean flag in the property changed event that gets set when any property is changed.

The ProxyTypeHelper keeps track of properties via the PropertyValues property. This stores the obvious value of the property as well as property information such as IsDirty property and the original property values. This can be used to compare with the changed value or rollback any changes.

ProxyTypeHelper includes a IsDirty property that checks the properties for any set IsDirty flags. There is also a RollBack method that will rollback to the object's original values

Note that currently IsDirty doesn't check any child objects for dirty values nor will it rollback any object values.

Final Words

This is still a work in progress. There are a number things that needs addressing, such as cloning/rolling back object properties. The most up to date source can be found at https://github.com/steinborge/ProxyTypeHelper.

The attached sample demonstrates the library as well as a concept of generic view models. There are a number of simple data maintenance screens exposing some SQLLite tables as well as a screen demonstrating some of the basic operations.

When running from development environment, you may get a not implemented exception, which doesn't affect the operation of library. Change your settings to ignore this exception.

The library was developed for WPF, but it wouldn't take much to modify parts for Silverlight or Win RT but since there is a fair bit of reflection, that might not work.

It works with Prism but have not tried with other MVVM frameworks such as MVVM Light or Caliburn. The ProxyLinker class was recently added to allow for framework integration. I am also investigating the ability to integrate Reactive extensions.

The features are mutually exclusive, so you don't need to implement 'wrapped' proxy properties/objects to use the command linking or dirty flag checking.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --