This post aims to provide a way to implement the Model View ViewModel (MVVM) architectural pattern using Plain Old CLR Objects (POCOs) while taking full advantage of .NET 4.0 DynamicObject Class.
In order to apply the Model View ViewModel (MVVM) architectural pattern we need:
- An instance of the View, (ex.: a UserControl type).
- An instance of the ViewModel, which in most scenarios is a class implementing the INotifyPropertyChanged interface (or inherits from a base class getting the implementation for free).
- An instance of the Model inside the ViewModel class, for getting the properties to display (and format them if necessary) and also for invoking commands on the model.
While we can not avoid step 1 (we need to have something to display to the user) and step 3 (we need to have something the user can read/edit), for basic scenarios we can try to avoid step 2.
Taking advantage of the .NET 4.0 and the DynamicObject Class, we can create a type deriving from the DynamicObject Class and specify dynamic behavior at run time. Furthermore, we can implement the INotifyPropertyChanged Interface on the derived type making it a good candidate for Data Binding.
Let's name our class, DynamicViewModel(Of TModel) Class. It must be able to:
- Accept references types (any class - a model is usually a class).
- Invoke public instance methods.
- Invoke public instance methods with arguments passed as CommandParameters.
- Get public instance properties.
- Set public instance properties.
- Notify callers when property change by raising the PropertyChanged event.
- If a property change results in chaning other properties, the caller must receive the notification for the other property changes too.
The DynamicViewModel(Of TModel) Class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Threading;
namespace DynamicViewModel
{
public sealed class DynamicViewModel<TModel>
: DynamicObject, INotifyPropertyChanged where TModel : class
{
private static readonly IDictionary<String, MethodInfo> s_methodInfos
= GetPublicInstanceMethods();
private static readonly IDictionary<String, PropertyInfo> s_propInfos
= GetPublicInstanceProperties();
private readonly TModel m_model;
private IDictionary<String, Object> m_propertyValues;
public DynamicViewModel(TModel model)
{
m_model = model;
NotifyChangedProperties();
}
public DynamicViewModel(Func<TModel> @delegate)
: this(@delegate.Invoke()) { }
public override Boolean TryInvokeMember(InvokeMemberBinder binder,
Object[] args, out Object result)
{
result = null;
MethodInfo methodInfo;
if (!s_methodInfos.TryGetValue(binder.Name,
out methodInfo)) { return false; }
methodInfo.Invoke(m_model, args);
NotifyChangedProperties();
return true;
}
public override Boolean TryGetMember(GetMemberBinder binder, out Object result)
{
var propertyValues = Interlocked.CompareExchange(
ref m_propertyValues, GetPropertyValues(), null);
if (!propertyValues.TryGetValue(binder.Name,
out result)) { return false; }
return true;
}
public override Boolean TrySetMember(SetMemberBinder binder, Object value)
{
PropertyInfo propInfo = s_propInfos[binder.Name];
propInfo.SetValue(m_model, value, null);
NotifyChangedProperties();
return true;
}
public void NotifyChangedProperties()
{
Interlocked.CompareExchange(
ref m_propertyValues, GetPropertyValues(), null);
IDictionary<String, Object> previousPropValues
= m_propertyValues;
IDictionary<String, Object> currentPropValues
= GetPropertyValues();
m_propertyValues
= currentPropValues;
foreach (KeyValuePair<String, Object> propValue
in currentPropValues.Except(previousPropValues))
{
RaisePropertyChanged(propValue.Key);
}
}
private static IDictionary<String, MethodInfo> GetPublicInstanceMethods()
{
var methodInfoDictionary = new Dictionary<String, MethodInfo>();
MethodInfo[] methodInfos = typeof(TModel).GetMethods(
BindingFlags.Public | BindingFlags.Instance);
foreach (MethodInfo methodInfo in methodInfos)
{
if (methodInfo.Name.StartsWith("get_") ||
methodInfo.Name.StartsWith("set_")) { continue; }
methodInfoDictionary.Add(methodInfo.Name, methodInfo);
}
return methodInfoDictionary;
}
private static IDictionary<String, PropertyInfo> GetPublicInstanceProperties()
{
var propInfoDictionary = new Dictionary<String, PropertyInfo>();
PropertyInfo[] propInfos = typeof(TModel).GetProperties(
BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo propInfo in propInfos)
{
propInfoDictionary.Add(propInfo.Name, propInfo);
}
return propInfoDictionary;
}
private IDictionary<String, Object> GetPropertyValues()
{
var bindingPaths = new Dictionary<String, Object>();
PropertyInfo[] propInfos = typeof(TModel).GetProperties(
BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo propInfo in propInfos)
{
bindingPaths.Add(
propInfo.Name,
propInfo.GetValue(m_model, null));
}
return bindingPaths;
}
private void RaisePropertyChanged(String propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler temp =
Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (temp != null)
{
temp(this, e);
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
The sample application for this post comes with a simple ContactView which has no specific viewModel but instead uses the DynamicViewModel(Of TModel) class.
Here is how the sample application looks:
The DynamicViewModel(Of TModel) Class is able to update the View which binds to an instance of this class.
Here is what the sample application does:
- Changing the First Name will result in changing the Full Name and the Reversed Full Name.
- The same rules apply when chaning the Last Name.
- The hyper-link is enabled only if the user presses the Clear Names button.
- The Clear Names button is enabled only when the Full Name text is not empty.
Here is the POCO model class that I have used:
using System;
public sealed class ContactDetails
{
public String FirstName
{
get
{
return m_firstName;
}
set
{
m_firstName = value;
SetFullName();
}
}
public String LastName
{
get
{
return m_lastName;
}
set
{
m_lastName = value;
SetFullName();
}
}
public String FullName
{
get;
set;
}
}
As you notice, this class does not implement any interface or base class. In fact, this class can be used successfully in ORM scenarios too (when you need to bind on the same classes that are used in your mappings).
Binding to methods
<StackPanel.CommandBindings>
<CommandBinding
Command="{x:Static m:ContactView.ClearNamesCommand}" />
<CommandBinding
Command="{x:Static m:ContactView.NavigateUriCommand}" />
</StackPanel.CommandBindings>
<Button
Content="Clear Names"
Command="{x:Static m:ContactView.ClearNamesCommand}" />
<Hyperlink
Command="{x:Static m:ContactView.NavigateUriCommand}"
CommandParameter="http://nikosbaxevanis.com"
NavigateUri="nikosbaxevanis.com">nikosbaxevanis.com</Hyperlink>
Binding to properties
<TextBox
Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged}"/>
Finally, I would like to show how the View's DataContext is initialized properly to accept the DynamicViewModel(Of TModel) Class wrapper around the model class:
Wiring view commands with methods of the model
internal partial class ContactView : UserControl
{
public static readonly RoutedCommand ClearNamesCommand = new RoutedCommand();
public static readonly RoutedCommand NavigateUriCommand = new RoutedCommand();
public ContactView()
{
InitializeComponent();
var instance = new ContactDetails() {
FirstName = "Nikos",
LastName = "Baxevanis"
};
dynamic viewModel = new DynamicViewModel<ContactDetails>(instance);
CommandManager.RegisterClassCommandBinding(typeof(ContactView),
new CommandBinding(
ClearNamesCommand,
(sender, e) => { viewModel.ClearFullName(); },
(sender, e) => { e.CanExecute = !String.IsNullOrWhiteSpace(viewModel.FullName); }));
CommandManager.RegisterClassCommandBinding(typeof(ContactView),
new CommandBinding(
NavigateUriCommand,
(sender, e) => { viewModel.NavigateTo(e.Parameter); },
(sender, e) => { e.CanExecute = String.IsNullOrWhiteSpace(viewModel.FullName); }));
DataContext = viewModel;
}
}
Notice that wiring between the ICommand Interface and the model class is done outside the dynamic ViewModel wrapper using the CommandManager Class which acts as a mediator between the View and the ViewModel. This give us the flexibility to define static reusable commands or specific commands for each view (as I've done above).
The project is hosted on CodePlex.
CodeProject