DotNetSlackers: ASP.NET News for lazy Developers

Friday, January 7, 2011

MVVM using POCOs with .NET 4.0 and the DynamicViewModel Class


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. 
Mvvm-sl-expando
In order to apply the Model View ViewModel (MVVM) architectural pattern we need:
  1. An instance of the View, (ex.: a UserControl type).
  2. 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).
  3. 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:
  1. Accept references types (any class - a model is usually a class).
  2. Invoke public instance methods.
  3. Invoke public instance methods with arguments passed as CommandParameters.
  4. Get public instance properties.
  5. Set public instance properties.
  6. Notify callers when property change by raising the PropertyChanged event.
  7. 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: 

 Collapse
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
    {
        /// <summary>
        /// Dictionary that holds information about the TModel public
        /// instance methods.
        /// </summary>
        /// <remarks>
        /// CA1810: Initialize reference type static fields inline.
        /// http://msdn.microsoft.com/en-us/library/ms182275(v=VS.100).aspx
        /// </remarks>
        private static readonly IDictionary<String, MethodInfo> s_methodInfos
            = GetPublicInstanceMethods();

        /// <summary>
        /// Dictionary that holds information about the TModel public
        /// instance properties.
        /// </summary>
        /// <remarks>
        /// CA1810: Initialize reference type static fields inline.
        /// http://msdn.microsoft.com/en-us/library/ms182275(v=VS.100).aspx
        /// </remarks>
        private static readonly IDictionary<String, PropertyInfo> s_propInfos
            = GetPublicInstanceProperties();

        private readonly TModel m_model;

        /// <summary>
        /// Dictionary that holds information about the current 
        /// values of the TModel public instance properties.
        /// </summary>
        private IDictionary<String, Object> m_propertyValues;

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="DynamicViewModel&lt;TModel&gt;"/> class.
        /// </summary>
        /// <param name="model">The model.</param>
        public DynamicViewModel(TModel model)
        {
            m_model = model;
            NotifyChangedProperties();
        }

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="DynamicViewModel&lt;TModel&gt;"/> class.
        /// </summary>
        /// <param name="delegate">The @delegate.</param>
        public DynamicViewModel(Func<TModel> @delegate)
            : this(@delegate.Invoke()) { }

        /// <summary>
        /// Provides the implementation for operations that invoke a member.
        /// </summary>
        /// <param name="binder">Provides information about the dynamicoperation.
        /// </param>
        /// <param name="args">The arguments that are passed to the 
        /// object member during the invoke operation.</param>
        /// <param name="result">The result of the member invocation.</param>
        /// <returns>
        /// true if the operation is successful; otherwise, false.
        /// </returns>
        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;
        }

        /// <summary>
        /// Gets the property value of the member.
        /// </summary>
        /// <param name="binder">The binder.</param>
        /// <param name="result">The result of the get operation. 
        /// For example, if the method is called for a property,
        /// you can assign the property value to 
        /// <paramref name="result"/>.</param>
        /// <returns>True with the result is set.</returns>
        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;
        }

        /// <summary>
        /// Sets the property value of the member.
        /// </summary>
        /// <param name="binder">The binder.</param>
        /// <param name="value">The value to set to the member. For example, 
        /// for sampleObject.SampleProperty = "Test", where sampleObject is 
        /// an instance of the class derived from the 
        /// <see cref="T:System.Dynamic.DynamicObject"/> class, 
        /// the <paramref name="value"/> is "Test".</param>
        /// <returns>True with the result is set.</returns>
        public override Boolean TrySetMember(SetMemberBinder binder, Object value)
        {
            PropertyInfo propInfo = s_propInfos[binder.Name];
            propInfo.SetValue(m_model, value, null);

            NotifyChangedProperties();
            return true;
        }

        /// <summary>
        /// Setting a property sometimes results in multiple properties
        /// with changed values too. For ex.: By changing the FirstName
        /// and the LastName the FullName will get updated. This method
        /// compares the m_propertyValues dictionary with the one that
        /// is obtained inside this method body. For each changed prop
        /// the PropertyChanged event is raised, notifying the callers.
        /// </summary>
        public void NotifyChangedProperties()
        {
            Interlocked.CompareExchange(
                ref m_propertyValues, GetPropertyValues(), null);

            // Store the previous values in a field.
            IDictionary<String, Object> previousPropValues
                = m_propertyValues;

            // Store the  current values in a field.
            IDictionary<String, Object> currentPropValues
                = GetPropertyValues();

            // Since we will be raising the PropertyChanged event
            // we want the caller to bind in the current values
            // and not the previous.
            m_propertyValues
                = currentPropValues;

            foreach (KeyValuePair<String, Object> propValue
                in currentPropValues.Except(previousPropValues))
            {
                RaisePropertyChanged(propValue.Key);
            }
        }

        /// <summary>
        /// Gets the public instance methods of the TModel type.
        /// </summary>
        /// <returns>
        /// A dictionary that holds information about TModel public
        /// instance properties.
        /// </returns>
        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;
        }

        /// <summary>
        /// Gets the public instance properties of the TModel type.
        /// </summary>
        /// <returns>
        /// A dictionary that holds information about TModel public
        /// instance properties.
        /// </returns>
        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;
        }

        /// <summary>
        /// Gets the property values about the TModel public instance properties.
        /// </summary>
        /// <returns>A dictionary that holds information about the current 
        /// values of the TModel public instance properties.</returns>
        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;
        }

        /// <summary>
        /// Raises the property changed event.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        private void RaisePropertyChanged(String propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Raises the <see cref="E:PropertyChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> 
        /// instance containing the event data.</param>
        private void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler temp =
                Interlocked.CompareExchange(ref PropertyChanged, null, null);

            if (temp != null)
            {
                temp(this, e);
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        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: 
DynamicMvvmApp
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: 
  1. Changing the First Name will result in changing the Full Name and the Reversed Full Name.
  2. The same rules apply when chaning the Last Name. 
  3. The hyper-link is enabled only if the user presses the Clear Names button. 
  4. 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:

 Collapse
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;
    }

    // (Less important members not shown)
} 

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 

 Collapse
<!-- Adding CommandBindings from XAML: -->
 <StackPanel.CommandBindings>
     <CommandBinding
         Command="{x:Static m:ContactView.ClearNamesCommand}" />
     <CommandBinding
         Command="{x:Static m:ContactView.NavigateUriCommand}" />
 </StackPanel.CommandBindings>
 
<!-- Binding a button click to a method with no arguments: -->
 <Button
     Content="Clear Names"
     Command="{x:Static m:ContactView.ClearNamesCommand}" />

<!-- Binding a hyperlink to a method passing the an argument via CommandParameter: -->
<Hyperlink
    Command="{x:Static m:ContactView.NavigateUriCommand}"
    CommandParameter="http://nikosbaxevanis.com"
    NavigateUri="nikosbaxevanis.com">nikosbaxevanis.com</Hyperlink> 

Binding to properties 

 Collapse
<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  

 Collapse
internal partial class ContactView : UserControl
{
    public static readonly RoutedCommand ClearNamesCommand  = new RoutedCommand();
    public static readonly RoutedCommand NavigateUriCommand = new RoutedCommand();
 
    public ContactView()
    {
        InitializeComponent();
 
        // Create a new instance. Once created
        // do not call methods directly on this
        // object. (Use the dynamic viewModel).
        var instance  = new ContactDetails() {
            FirstName = "Nikos",
            LastName  = "Baxevanis"
        };
 
        dynamic viewModel = new DynamicViewModel<ContactDetails>(instance);
 
        // Wire the ClearNamesCommand from the view to the viewModel.
        CommandManager.RegisterClassCommandBinding(typeof(ContactView),
            new CommandBinding(
                ClearNamesCommand,
                (sender, e) => { viewModel.ClearFullName(); },
                (sender, e) => { e.CanExecute = !String.IsNullOrWhiteSpace(viewModel.FullName); }));
 
        // Wire the NavigateUriCommand from the view to the viewModel.
        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 theCommandManager 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).

No comments:

Post a Comment