MVVV views / viewmodel WPF mediator : how to tame a grid without any codebehind

So we're all doing WPF/MVVM by now, great, at least we get some nice declarative UI paradigm, where everything is concise and cleanly separated, your XAML doesn't have any codebehind and all is well.

That's until you start using third-party components in your views (say, a grid for example). Most vendors don't ship MVVM-friendly WPF controls, so to interact with these controls, you ususally have two choices:
  1.  Wrapping your control, this never ends nicely
  2. Add event handlers in your view to take care of your components.
I am not satisfied by either method, as wrapping a grid can only end in tears of blood, and event handlers sprinkled all throughout your view will break separation of concern, your view, on top of having an ugly codebehind, now potentially has to pamper serveral controls. And these event handlers will end-up being copypasted everywhere.

Let's see if there isn't a third way we could use, where we still get the benefits of custom event handling without anything seeping in the view itself.



Assume we're handling a grid, we will need to control sorting, grouping and user selection, and maybe user input, that's 4 different functional areas to cover for the view, let's see if we can abstract these behaviors and maybe reuse them in other views using the same components.

So, first, how to get rid of event handlers?


The answer is you don't, but what you can do is delegate this away from the view to a mediator that can be exposed through a (mockable) interface to your ViewModel.

The goal is to keep the view and viewmodel separated and communicate back and forth through either Binding or clean interfaces (ie without dependencies to WPF so we can unit test the ViewModel)

We can create a mediator class that will live in-between both layers and contain all these dirty event handlers internally, and expose a clean API to the ViewModel. By using attached properties, we can link this mediator to both the View and the ViewModel in the XAML.

Less talk, more code, here's a sample XAML where I use this technique:

<!-- This is our view's XAML -->
<igDP:XamDataGrid DataSource="{Binding Messages}"
                  mediators:GridLayoutMediator.Owner="{Binding}"
                  />


Now let's have a look at the ViewModel :
// The viewmodel
internal class MyViewModel: IGridLayoutOwner
{
    #region Grid Layout Owner implementation
    public IGridLayoutMediator Mediator { get; set; }

    readonly MemoryStream _stream = new MemoryStream();

    // exposed through relay command
    public void WouldYouKindlySave()
    {
        _stream.SetLength(0);
        Mediator.SaveLayout(_stream);
    }

    // exposed through relay command
    public void WouldYouKindlyRestore()
    {
        _stream.Position = 0;
        Mediator.RestoreLayout(_stream);
    }

    #endregion
    // .. stuff ..
}


Defining the Mediator and Mediator Owner Interfaces

The mediator owner is the viewmodel, the mediator knows who the owner is, here goes:


public interface IGridLayoutMediator : IMediator<IGridLayoutOwner, IGridLayoutMediator>
{
    void SaveLayout(Stream stream);
    void RestoreLayout(Stream stream);
}

public interface IGridLayoutOwner : IMediatorOwner<IGridLayoutOwner, IGridLayoutMediator>
{
    void WouldYouKindlySave();
    void WouldYouKindlyRestore();
}

public class GridLayoutMediator : MediatorBase<IGridLayoutOwner, IGridLayoutMediator, XamDataGrid, GridLayoutMediator>, IGridLayoutMediator
{
    protected override XamDataGrid View { get; set; }

    public void SaveLayout(Stream stream)
    {
        View.SaveCustomizations(stream);
    }

    public void RestoreLayout(Stream stream)
    {
        View.LoadCustomizations(stream);
    }
}


From this we can derive a common base class that handles this pattern:



    ///<summary>Base mediator interface</summary>
    public interface IMediator<TOwner, TMediator> : IDisposable
        where TOwner : IMediatorOwner<TOwner, TMediator>
        where TMediator : IMediator<TOwner, TMediator>
    {
    }

    ///<summary>Base mediator owner interface</summary>

    public interface IMediatorOwner<TOwner, TMediator>
        where TOwner : IMediatorOwner<TOwner, TMediator>
        where TMediator : IMediator<TOwner, TMediator>
    {
        ///<summary>The mediator instance</summary>
        TMediator Mediator { get; set; }
    }

    /// <summary>
    /// This class abstracts-away the mediator - owner - view binding.
    /// <remarks>Don't be afraid by the number of template arguments
    /// they are only used for real ultimate type safety power</remarks>
    /// </summary>
    public abstract class MediatorBase<TOwner, TMediator, TView, TImpl> : IMediator<TOwner, TMediator>
        where TOwner : class, IMediatorOwner<TOwner, TMediator> // owner
        where TMediator : IMediator<TOwner, TMediator> // mediator interface
        where TView : DependencyObject // the view
        where TImpl : MediatorBase<TOwner, TMediator, TView, TImpl>, TMediator, new() // trick : the inheritor of this very class
    {
        ///<summary>The mediator owner attached property</summary>
        public static readonly DependencyProperty OwnerProperty = DependencyProperty.RegisterAttached(
            "Owner", typeof(TOwner), typeof(MediatorBase<TOwner, TMediator, TView, TImpl>),
            new FrameworkPropertyMetadata(OnChange)
            );

        /// <summary>The view we attached the property on</summary>
        protected abstract TView View { get; set; }

        /// <summary> Owner of this mediator</summary>
        public TOwner Owner { get; private set; }
       
        /// <summary>
        /// Does the link between Mediator to View and Mediator to Owner
        /// </summary>
        private static void OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // unhook the previous mediator
            var oldOwner = e.OldValue as TOwner;
            if (oldOwner != null)
            {
                var mediator = oldOwner.Mediator as TImpl;

                if (mediator != null)
                {
                    oldOwner.Mediator = default(TMediator);
                    mediator.Dispose();
                }
            }
            // create a new mediator instance and register it
            var newOwner = e.NewValue as TOwner;
            if (newOwner != null)
            {
                newOwner.Mediator = new TImpl
                                        {
                                            Owner = newOwner,
                                            View = (TView)d,
                                        };
            }
        }
        public static void SetOwner(UIElement site, TOwner owner)
        {
            site.SetValue(OwnerProperty, owner);
        }

        public static TOwner GetLayoutOwner(UIElement site)
        {
            return (TOwner)site.GetValue(OwnerProperty);
        }
 
        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
        }
    }



The thing I particularly like about this is the fact that I can have several mediators attached to the same view, ie I have a GridLayoutMediator, a GridUserFeedbackMediator and so on, in the end, the mediator encapsulates the behavior of a very complex control, and the view remains code-less, while the ViewModel only handles commands and passes-on messages to the mediator.

Of course, nothing prevents you from having the ViewModel register events against the mediator if you need feedback, just bear in mind ViewModels need to be concise...

Hope that helps,

Florian


No comments:

Post a Comment

Please leave your comments in English or French and I will be pleased to answer them if you have any questions.

Spammers will be walked down the plank matey. Arrr!