Brian Genisio's House of Bilz

  Home  |   Contact  |   Syndication    |   Login
  62 Posts | 0 Stories | 118 Comments | 0 Trackbacks

News

Locations of visitors to this page

Archives

Post Categories

Who am I?

More Adventures in MVVM Shout it kick it on DotNetKicks.com

One of the biggest points of friction for me when implementing the MVVM pattern happens when I need to bind commands to events.  When using Prism, I get the Button.Click command binding out of the box, but every other event needs to be wired up individually.  Doing this requires a LOT of boilerplate code that is very easy to get wrong.  In my last post, I published some code to alleviate that pain.  Still, it requires you to write a new behavior and attachment for every event that you want to bind an event to.

For a while now, I have toyed with the idea of just binding commands to events directly.  I ran into a lot of bumps in the road.  For instance, every event handler has a different event argument type.  This requires all of the handlers to be dynamic.  I also couldn’t create an inline command binding – I will surely want to bind more than one event per control – so I need to create a collection of bindings.  Creating arrays of structures created its own troubles – binding only works with FrameworkElements within the visual tree.  This required me to write my own crude binding within my generic behavior.

What follows is very loosely based off of the Chinch MVVM framework.  I tested this code in Silverlight and WPF and it works really well!

Assume I have a ViewModel that looks like this:

public class MainPageViewModel : INotifyPropertyChanged
{
    ...
    public ICommand MouseLeaveCommand { get; private set; }
    public ICommand MouseEnterCommand { get; private set; }
    public ICommand ClickCommand { get; private set; }
    ...
}

I can then bind the commands to events on a control (Button, for instance):

<Button Content="Click Me">
    <Behaviors:Events.Commands>
        <Behaviors:EventCommandCollection>
            <Behaviors:EventCommand CommandName="MouseEnterCommand" EventName="MouseEnter" />
            <Behaviors:EventCommand CommandName="MouseLeaveCommand" EventName="MouseLeave" />
            <Behaviors:EventCommand CommandName="ClickCommand" EventName="Click" />
        </Behaviors:EventCommandCollection>
    </Behaviors:Events.Commands>
</Button>

I no longer need to write any extra code whenever I want to attach commands to my events!  There are a few caveats to this code:

  1. The XAML requires the EventCommandCollection to be declared in the XAML.  I struggled to figure out how to eliminate this but gave up.  Someone smarter than me might be able to tell me what I am doing wrong.
  2. This code does not consider command properties.  Every command assumes a null parameter.  If you need parameters (like data context), then you’ll have to do something differently (either use the old-school mechanism or extend this code to handle some special event types).
  3. You don’t bind directly to the command.  Instead, you declare the name of the command (Notice CommandName is not bound).  The behavior binds for you using a primitive mechanism.

Here is the command behavior that does all the work:

public class Events
{
    private static readonly DependencyProperty EventBehaviorsProperty =
        DependencyProperty.RegisterAttached(
        "EventBehaviors",
        typeof(EventBehaviorCollection),
        typeof(Control),
        null);

    private static readonly DependencyProperty InternalDataContextProperty =
        DependencyProperty.RegisterAttached(
        "InternalDataContext",
        typeof(Object),
        typeof(Control),
        new PropertyMetadata(null, DataContextChanged));

    private static void DataContextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var target = dependencyObject as Control;
        if (target == null) return;

        foreach (var behavior in GetOrCreateBehavior(target))
            behavior.Bind();
    }

    public static readonly DependencyProperty CommandsProperty =
        DependencyProperty.RegisterAttached(
        "Commands",
        typeof(EventCommandCollection),
        typeof(Events),
        new PropertyMetadata(null, CommandsChanged));

    public static EventCommandCollection GetCommands(DependencyObject dependencyObject)
    {
        return dependencyObject.GetValue(CommandsProperty) as EventCommandCollection;
    }

    public static void SetCommands(DependencyObject dependencyObject, EventCommandCollection eventCommands)
    {
        dependencyObject.SetValue(CommandsProperty, eventCommands);
    }

    private static void CommandsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var target = dependencyObject as Control;
        if (target == null) return;

        var behaviors = GetOrCreateBehavior(target);
        foreach (var eventCommand in e.NewValue as EventCommandCollection)
        {
            var behavior = new EventBehavior(target);
            behavior.Bind(eventCommand);
            behaviors.Add(behavior);
        }

    }

    private static EventBehaviorCollection GetOrCreateBehavior(FrameworkElement target)
    {
        var behavior = target.GetValue(EventBehaviorsProperty) as EventBehaviorCollection;
        if (behavior == null)
        {
            behavior = new EventBehaviorCollection();
            target.SetValue(EventBehaviorsProperty, behavior);
            target.SetBinding(InternalDataContextProperty, new Binding());
        }

        return behavior;
    }
}

public class EventCommand
{
    public string CommandName { get; set; }
    public string EventName { get; set; }
}

public class EventCommandCollection : List<EventCommand>
{
}

public class EventBehavior : CommandBehaviorBase<Control>
{
    private EventCommand _bindingInfo;

    public EventBehavior(Control control)
        : base(control)
    {

    }

    public void Bind(EventCommand bindingInfo)
    {
        ValidateBindingInfo(bindingInfo);

        _bindingInfo = bindingInfo;

        Bind();
    }

    private void ValidateBindingInfo(EventCommand bindingInfo)
    {
        if(bindingInfo == null) throw new ArgumentException("bindingInfo");
        if (string.IsNullOrEmpty(bindingInfo.CommandName)) throw new ArgumentException("bindingInfo.CommandName");
        if (string.IsNullOrEmpty(bindingInfo.EventName)) throw new ArgumentException("bindingInfo.EventName");
    }

    public void Bind()
    {
        ValidateBindingInfo(_bindingInfo);
        HookPropertyChanged();
        HookEvent();
        SetCommand();
    }

    public void HookPropertyChanged()
    {
        var dataContext = TargetObject.DataContext as INotifyPropertyChanged;
        if (dataContext == null) return;

        dataContext.PropertyChanged -= DataContextPropertyChanged;
        dataContext.PropertyChanged += DataContextPropertyChanged;
    }

    private void DataContextPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == _bindingInfo.CommandName)
            SetCommand();
    }

    private void SetCommand()
    {
        var dataContext = TargetObject.DataContext;
        if (dataContext == null) return;

        var propertyInfo = dataContext.GetType().GetProperty(_bindingInfo.CommandName);
        if (propertyInfo == null) throw new ArgumentException("commandName");

        Command = propertyInfo.GetValue(dataContext, null) as ICommand;
    }

    private void HookEvent()
    {
        var eventInfo = TargetObject.GetType().GetEvent(
            _bindingInfo.EventName, BindingFlags.Public | BindingFlags.Instance);
        if (eventInfo == null) throw new ArgumentException("eventName");

        eventInfo.RemoveEventHandler(TargetObject, GetEventMethod(eventInfo));
        eventInfo.AddEventHandler(TargetObject, GetEventMethod(eventInfo));
    }

    private Delegate _method;
    private Delegate GetEventMethod(EventInfo eventInfo)
    {
        if (eventInfo == null) throw new ArgumentNullException("eventInfo");
        if (eventInfo.EventHandlerType == null) throw new ArgumentException("EventHandlerType is null");

        if (_method == null)
        {
            _method = Delegate.CreateDelegate(
                eventInfo.EventHandlerType, this,
                GetType().GetMethod("OnEventRaised",
                BindingFlags.NonPublic | BindingFlags.Instance));
        }

        return _method;
    }

    private void OnEventRaised(object sender, EventArgs e)
    {
        ExecuteCommand();
    }
}

public class EventBehaviorCollection : List<EventBehavior>
{ }
posted on Thursday, August 27, 2009 1:27 PM

Feedback

# re: Adventures in MVVM – Binding Commands to ANY Event 8/29/2009 11:31 AM Rob
I thoroughly recommend that you investigate Caliburn. Here's a link to the TOC: http://caliburn.codeplex.com/Wiki/View.aspx?title=Table%20Of%20Contents Be sure to read the section on actions (note the short behavior). You may also find the section on IResult interesting.

# re: Adventures in MVVM – Binding Commands to ANY Event 8/29/2009 11:46 AM Eric Polerecky
Is there and advantages to this method over prism's event aggragator?

The more I lean about EA; the more I start to think that's the only way I'll ever do events in silverlight. Maybe it's because I'm coming from a heavy JavaScript background but I love the unobtrusive nature of the EA pattern.

# re: Adventures in MVVM – Binding Commands to ANY Event 8/29/2009 11:59 AM Brian Genisio
@Eric: I am quite fond of the Event Aggregator as well... I use it and find it extremely useful. Is there a way to bind events in XAML?

I use it to communicate from ViewModel to ViewModel, but when I want to execute a command in the ViewModel that is bound to the View, I find this mechanism really useful. Mostly because I don't want to broadcast that a button was clicked in the EA. I want the mouse click event to bind to a command in my ViewModel, and let the ViewModel decide what it means to have the command be executed. If the ViewModel wants to send an event to other components using the EA, then it can do so.

Do you use the EventAggregator differently?

# re: Adventures in MVVM – Binding Commands to ANY Event 8/29/2009 12:56 PM Brian Genisio
@Rob: My biggest problem with Caliburn is my Lawyers. I work for a big company and open source software is a bear to get past them. (You should have seen the hoops I needed to jump through to use SQLite.) I got them to be OK with Prism because it was put out by Microsoft. I don't like this limitation, but it "is what it is". Because of this, I blog from the perspective... getting MVVM to work from the basic tools. Although, it is starting to get tiresome to be doing this much plumbing.

That being said, I have spent some time looking into Caliburn, I've got to say that it looks nice. When I give my MVVM talk, I include it as an option for third-party support for MVVM (along with the MVVM Lite toolkit, SilverlightFX and Prism).

It has been my plan for several months to dig into Caliburn and really get to know it. It might not be something I can use at my job, but I think it is something that I should be more familiar with. I will be sure to blog about it when that happens.

Thanks for the tip!

# re: Adventures in MVVM – Binding Commands to ANY Event 8/31/2009 7:55 AM Ward Bell
With respect to your concerns:

1.The XAML requires the EventCommandCollection to be declared in the XAML.

I would have thought you could declare EventCommandCollection to be the “Content” property by marking the class with the ContentPropertyAttribute. This works even for collection properties.

I know you can do it in WPF … see http://msdn.microsoft.com/en-us/library/ms752059.aspx#contentmodels and http://msdn.microsoft.com/en-us/library/system.windows.markup.contentpropertyattribute.aspx.

We see examples of it all the time in SL, e.g., StackPanel (you never have to say <StackPanel.Children> … it’s implied).

2.This code does not consider command properties.

A good omission in my opinion. I don’t like command parameters. First you have one .. then you want a second. It’s all downhill. Instead, if there is a parameter to pass to the VM from the View, it should be done by binding the parameter source to a VM property. David Hill agrees … for what that is worth.

3.You don’t bind directly to the command. Instead, you declare the name of the command (Notice CommandName is not bound).

Inescapable … like all the other string binding that goes on in XAML. At least you get Intellisense with some things you do; not here. Still I dislike all of the magic string madness. Nothing to be done that I can see.

Of course you could provide a binding alternative that would yield to Intellisense. The extra syntax doesn’t seem worth it to me.

Post A Comment
Title:
Name:
Email:
Website:
Comment:
Verification: