Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / XAML

Dynamic View Model

Rate me:
Please Sign up or sign in to vote.
4.89/5 (9 votes)
28 Jul 2014CPOL5 min read 62.3K   1.2K   30   4
A technique for generating ViewModel objects dynamically.

Introduction

The Model-View-ViewModel pattern is a powerful tool that leverages the DataBinding features of WPF. However, writing the ViewModel classes can be tedious and time-consuming. In this article I describe a technique which can be used to dynamically generate the ViewModel layer at runtime.

This article is now part of a series. Part 2 of the series can be found below:

Dynamic View Model Part 2: Key/Ref

Background   

Typically when developing an application in WPF, there are two layers of data behind the interface. The lowest layer is the Model, which can be a thin data layer or a proxy. Above that you have the ViewModel layer, which exposes model properties to XAML and provides 2-way or push bindings from XAML to C#. The state of the user-interface in WPF is almost non-deterministic; it cannot be queried reliably for any information that might be useful for interface logic (such as height, width, expansion, selection, focus, etc). This information can be stored at the ViewModel layer so that there is always a reliable record of the interface state. In addition, Undo and other services can be hosted at the ViewModel layer using RelayCommand objects. This is the approach I use in this article.

Pros and Cons

Pros:

 

  • Improves programmer productivity by eliminating the time spent writing ViewModel classes.
  • When the Model layer changes, no updates are needed for the ViewModel layer.
  • Undo, Add, Remove, MoveUp, MoveDown, and shared selection are supported out-of-the-box. 
  • Provides a clear separation between the domain data and interface data.
  • The model only needs to implement INotifyPropertyChanged. The model is not required to derive from a specific class and it is not assumed to contain any interface data. 

Cons:

 

 

  •  While data is represented well using this technique, methods are not. Any custom functions required at the ViewModel layer will require a specialized derived class.
  • If you don't want some of the features provided by the ViewModel layer, it is not possible to turn them off for a specific model. 

 

The Technique

There are three major classes at work: ViewModelProperty, ViewModelCollection, and DynamicViewModel.

ViewModelProperty holds a reference to the ModelContext, the NotifyPropertyChanged event code of the host ViewModel, a PropertyInfo object, and a reference to the ViewModelManager or UndoManager. Changes made at the model layer update the CachedValue field, while changes at the interface layer are captured and encapsulated as commands for the UndoManager.

ViewModelCollection performs a similar role in that it listens to changes at the model layer. It differs in that commands are exposed to the interface to allow for manipulation. ViewModelCollection adds items to the Model list using Activator when the AddCommand is invoked. The Type provided to Activator is accessed using the following code: 

C#
m_modelType = modelCollection.GetType().GetGenericArguments()[0]; 

ViewModelProperty and ViewModelCollection both use weak event patterns to listen to model changes, thereby preventing the ViewModel objects from persisting in memory. The example requires .NET 4.5 but it can be easily ported to .NET 4.0 by using custom weak event managers. 

The core of this technique is located in the DynamicViewModel class in the method LoadProperties.  

C#
private void LoadProperties()
{
    m_properties = new Dictionary<string, object>();
    Type type = m_modelContext.GetType();
    var reflectedProperties = type.GetProperties();
    foreach (var reflectedProperty in reflectedProperties)
    {
        if (typeof(IObservableList).IsAssignableFrom(reflectedProperty.PropertyType))
        {
            m_properties.Add(reflectedProperty.Name, new ViewModelCollection(reflectedProperty.GetValue(m_modelContext) as IObservableList, this, m_manager));
        }
        else
        {
            m_properties.Add(reflectedProperty.Name, new ViewModelProperty(m_modelContext, reflectedProperty.Name, m_manager));
        }
    }
}  

This method creates a ViewModelProperty instance for primitive properties, and a ViewModelCollection instance for collection properties. DynamicViewModel inherits from DynamicObject. It overrides two methods: TryGetMember and TrySetMember.

C#
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (m_properties.ContainsKey(binder.Name))
    {
        object member = m_properties[binder.Name];
        if (member is ViewModelCollection)
        {
            result = member;
        }
        else
        {
            ViewModelProperty property = member as ViewModelProperty;
            result = property.CachedValue;
        }
        return true;
    }
    return base.TryGetMember(binder, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
    if (m_properties.ContainsKey(binder.Name))
    {
        object member = m_properties[binder.Name];
        if(member is ViewModelProperty)
        {
            ViewModelProperty property = member as ViewModelProperty;
            property.CachedValue = value;
            return true;
        }
    }
    return base.TrySetMember(binder, value);
} 

This makes ViewModelProperty and ViewModelCollection objects visible to XAML. 

Using the code

1) Define your model classes. For collections, use ObservableList. 

Example:

C#
[XmlArray("Children")]
[XmlArrayItem("ChildModel")]
public ObservableList<ChildModel> Children
{
    get
    {
        return m_children;
    }
    set
    {
        m_children = value;
        NotifyPropertyChanged("Children");
    }
}  

2) Create a new DynamicViewModel to use as the DataContext. You can also create a ProjectManager to host the DynamicViewModel root instance (not covered here). For the root VM, pass the following into the constructor: 

 

  • A new instance of the root Model class. 
  • null 
  • A new instance of a ViewModelManager. 

 

Example:  

C#
DataContext = new DynamicViewModel(new ParentModel(), null, new ViewModelManager());  

3) To access the Undo commands, bind to the UndoCommand and RedoCommand RelayCommand objects.

Example:

XML
<KeyBinding Key="Z" Modifiers="Control" Command="{Binding Path=Manager.UndoManager.UndoCommand}"/>   

4) To bind to a property, use the standard syntax. 

Example:

XML
Text="{Binding Path=Name, UpdateSourceTrigger=LostFocus, Mode=TwoWay}" 

5) You can use standard syntax to bind to a collection as well. 

Example:

XML
ItemsSource="{Binding Path=Children}"  

6) To bind selection to the SharedVisualState instance, add a Setter to the ItemContainerStyle property of the items control.

Example:

XML
<Setter Property="IsSelected" Value="{Binding Path=SharedVisualState.IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>    

7) Use the RelayCommand properties on ViewModelCollection to extend your UI with Add, Remove, MoveUp, and MoveDown features. As long as IsSelected is bound to the interface item, these four commands will automatically work. 

Example:

XML
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.AddCommand}">Add Child</Button>
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.RemoveCommand}">Remove Child</Button>
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.MoveUpCommand}">Move Up</Button>
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.MoveDownCommand}">Move Down</Button> 

Points of Interest 

Notice the ObservableList class and interface. 

C#
public interface IObservableList : ICollection, IList, INotifyCollectionChanged, INotifyPropertyChanged
{
    void Move(int oldIndex, int newIndex);
}
public class ObservableList<T> : ObservableCollection<T>, IObservableList
{
    public ObservableList()
        : base()
    {
    }
    public ObservableList(IEnumerable<T> collection)
        : base(collection)
    {
    }
    public ObservableList(IList<T> list)
        : base(list)
    {
    }
    void IObservableList.Move(int oldIndex, int newIndex)
    {
        Move(oldIndex, newIndex);
    }
} 

This code extends ObservableCollection with a template-independant Move method for use by ChangeCollectionCommand. This is just one example of how ObservableCollection can be extended or replaced entirely to add specific features to the system. 

Another thing to note is that multiple ViewModel instances can share the same visual state. Notice how the same item is selected in both windows in the screenshot below, even though each window has a different ViewModel tree. 

Image 1

This is accomplished using a ConditionalWeakTable, located in the System.Runtime.CompilerServices namespace. ViewModelManager associates Model objects with a SharedVisualState, and when Models are garbage collected, the state is lost as well. 

History

  • Initial article. 6/29/2013
  • Removed the need to decorate collections with a special attribute and added the indexer to ViewModelParentUtility. 6/30/2013
  • Changed the technique to use DynamicObject. My thanks to FatCatProgrammer for pointing it out. 7/1/13
  • Changed SetPropertyCommand to use a WPF type converter on incoming data, to prevent crashes caused by type casting errors. 11/30/13 
  • Added a link to Part 2. 7/25/14

Preview For Part 2

I added a system for key/value pairs, so that Model objects can be associated with each other. A ComboBox can be populated using the ViewModelParentUtility class, and a selected instance updates the key field in the Model.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Junior)
United States United States
I am a tools programmer in the game industry. I primarily work on build/pipeline and productivity tools in C# and WPF.

Comments and Discussions

 
QuestionViewModelPropery constructor should get PropertyInfo as parameter, so we dont do reflection twice ~ Pin
DenisGbrg9-Feb-14 6:32
DenisGbrg9-Feb-14 6:32 
GeneralMy vote of 5 Pin
Wooters1-Jul-13 10:39
Wooters1-Jul-13 10:39 
Questioncommentg Pin
FatCatProgrammer1-Jul-13 5:14
FatCatProgrammer1-Jul-13 5:14 
AnswerRe: commentg Pin
Bryan Croteau1-Jul-13 6:08
Bryan Croteau1-Jul-13 6:08 
I want to extend the model with services that are not needed for simple data loading/transfer. Usually in my applications the model layer is very thin with no logic, with fields for preserving unrecognized XML during serialization. DynamicObject is another solution that I will look into, thanks for the suggestion.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.