.NET & Funky Fresh

Syndication

News

  • <script type="text/javascript" src="http://ws.amazon.com/widgets/q?ServiceVersion=20070822&amp;MarketPlace=US&amp;ID=V20070822/US/bluspiconinc-20/8001/8b68bf4b-6724-40e7-99a5-a6decf6d8648"> </script>
MVVM Study Part 5: Convention over Configuration

In the last part I showed a very basic view model. I needed something in order to give those new to the concept of MVVM a concrete example. However, there are several aspects of that example that left me feeling uncomfortable; not to mention the myriad questions that seemed to arise in my mind about how something like this would work in a real-world situation. In this installment, I want to look at something simple we can do to improve the developer experience.

One of the things that bugs me about all the MVVM implementations I see (including the one I showed in part 4) is the need for command properties on the view model. Generally speaking, I think it’s good to remove as much infrastructure code as possible from view models, so that the intent of the view model is made clearer. It’s actually quite easy to do this if we establish a few simple conventions as follows:

  1. All View Model methods with the same name as a View’s ICommandSource should be wired as Commands.
  2. All properties that follow the name convention Can{MethodName} should be used as the “CanExecute” implementation of the Command that has been wired to the corresponding method.

Before we can make this work, we need to replace our old DelegateCommand with a new ReflectiveCommand:

public class ReflectiveCommand : ICommand
{
    private readonly object _model;
    private readonly PropertyInfo _canExecute;
    private readonly MethodInfo _execute;

    public ReflectiveCommand(object model, MethodInfo execute, PropertyInfo canExecute)
    {
        _model = model;
        _execute = execute;
        _canExecute = canExecute;

        var notifier = _model as INotifyPropertyChanged;
        if (notifier != null && _canExecute != null)
        {
            notifier.PropertyChanged += (s, e) =>{
                if (e.PropertyName == _canExecute.Name)
                    CanExecuteChanged(this, EventArgs.Empty);
            };
        }
    }

    public void Execute(object parameter)
    {
        _execute.Invoke(_model, null);
    }

    public bool CanExecute(object parameter)
    {
        if (_canExecute != null)
            return (bool)_canExecute.GetValue(_model, null);
        return true;
    }

    public event EventHandler CanExecuteChanged = delegate { };
}

ReflectiveCommand allows us to easily enable a command to execute an arbitrary method and to raise its CanExecuteChanged event whenever a particular property changes.  By combining this with our conventions, we can easily bridge the gap between our view and view model. Let’s take a look at a simple binder service that applies conventions to a View/View Model pair:

public static class Binder
{
    public static void Bind(object viewModel, FrameworkElement view)
    {
        var methods = viewModel.GetType().GetMethods();
        var properties = viewModel.GetType().GetProperties();

        foreach(var method in methods)
        {
            var foundControl = view.FindName(method.Name);
            if(foundControl == null) continue;

            var foundProperty = properties
                .FirstOrDefault(x => x.Name == "Can" + method.Name);

            var command = new ReflectiveCommand(viewModel, method, foundProperty);
            TrySetCommand(foundControl, command);
        }

        view.DataContext = viewModel;
    }

    private static void TrySetCommand(object control, ICommand command)
    {
        if (!TrySetCommandBinding<ButtonBase>(control, ButtonBase.CommandProperty, command))
            if (!TrySetCommandBinding<MenuItem>(control, MenuItem.CommandProperty, command))
                TrySetCommandBinding<Hyperlink>(control, Hyperlink.CommandProperty, command);
    }

    private static bool TrySetCommandBinding<T>(object control, DependencyProperty property, ICommand command)
        where T : DependencyObject, ICommandSource
    {
        var commandSource = control as T;
        if(commandSource == null) return false;
         
        BindingOperations.SetBinding(commandSource, property, new Binding { Source = command });
        return true;
    }
}

The binder uses reflection to look for conventions between the provided View and View Model. If found, it creates instances of ReflectiveCommand and binds them appropriately. Finally, it sets the View Model to the View’s DataContext.

Taking the same example as in Part 4, we can now create our View Model as follows:

public class CreateEmployeeViewModel : PropertyChangedBase
{
    private string _firstName;
    private string _lastName;

    public string FirstName
    {
        get { return _firstName; }
        set
        {
            _firstName = value; 
            NotifyOfPropertyChange("FirstName");
            NotifyOfPropertyChange("CanSave");
        }
    }

    public string LastName
    {
        get { return _lastName; }
        set
        {
            _lastName = value; 
            NotifyOfPropertyChange("LastName");
            NotifyOfPropertyChange("CanSave");
        }
    }

    public bool CanSave
    {
        get { return !string.IsNullOrEmpty(FirstName) && !string.IsNullOrEmpty(LastName); }
    }

    public void Save()
    {
        MessageBox.Show("Saved.");
    }
}

Notice the absence of commands.  I also factored out the following base class:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public virtual event PropertyChangedEventHandler PropertyChanged = delegate { };

    public void NotifyOfPropertyChange(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

We’ll use the same view as last time, but without the command bindings:

<Window x:Class="MVVMStudy.PartFive.CreateEmployeeView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        SizeToContent="Height"
        Width="400">
    <Grid Margin="4">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        
        <Label>First Name:</Label>
        <TextBox Text="{Binding FirstName}" 
                 Grid.Column="1"/>
        
        <Label Grid.Row="1">Last Name:</Label>
        <TextBox Text="{Binding LastName}" 
                 Grid.Row="1"
                 Grid.Column="1"/>
        
        <Button Content="Save"
                x:Name="Save"
                HorizontalAlignment="Right" 
                Margin="0 4 0 0"
                Grid.Row="2"
                Grid.ColumnSpan="2"/>
    </Grid>
</Window>

And the code behind becomes as simple as this:

public partial class CreateEmployeeView : Window
{
    public CreateEmployeeView()
    {
        InitializeComponent();
        Binder.Bind(new CreateEmployeeViewModel(), this);
    }
}

If you are willing to establish a few conventions for your application, it’s pretty amazing how much simpler your life can become…


Posted 12-14-2009 5:33 PM by Rob Eisenberg

[Advertisement]

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)