Wednesday, September 30, 2009

Silverlight/Prism ViewModel and DelegateCommand

In yesterday's post about Decoupled ChildWindow Dialogs in Silverlight using Prism, I demonstrated a way to use EventAggregator to decouple the implementation of a dialog from the code that requires the confirmation. In one example, I showed a code-behind click event that fired off the process, something like this:

private void Button_Delete_Click(object sender, System.Windows.RoutedEventArgs e)
{
    Button src = e.OriginalSource as Button;
    if (src != null)
    {
        _eventService.GetEvent<MessageBoxEvent>().Publish(
            MessageBoxPayload.GeneratePayLoad(src.DataContext, true, "Please Confirm", "Are you sure you wish to delete this item?",
                                      DeleteItem));
    }
}    

This, in my opinion, is way too much information for a view to know and understand. It has to know about my event aggregator service? Is that really necessary?

If you are not familiar with the Model-View-ViewModel (MVVM) pattern, you'll want to Google/research it now. It is important because a lot of the marshalling of data will happen in the ViewModel, and the view will simply be bound to the view model.

Prism provides a commanding interface that works well with buttons. You can use an attached property to bind the click to a command (which is in turn a DelegateCommand). The command will disable the button if it can't execute and fire when the button is clicked. The button becomes a binding, like this:

...
<Button Commands:Click.Command={Binding SearchCommand} .../> 
...

In the view model, the SearchCommand is defined like this:

...
public DelegateCommand<object> SearchCommand { get; set; }
...
SearchCommand = new DelegateCommand<object>(o => 
   _service.GetEventService().GetEvent<SearchEvent>().Publish(
      SearchCriteria),o => _CanSearch());

In this example, we are assuming the form fields are bound to a search criteria object in the view model. Presumably once the criteria meet our validation requirements, _CanSearch will return true, the button will be enabled, and the command will publish an event to begin the search using the populated criteria entity.

While I intend to dig more in depth in that pattern, I wanted to present something I believe is a common issue and describe how I addressed it. In this case, the view model contains the search criteria as well as a collection of objects for the results grid. This becomes problematic with binding, because my grid row is bound to the item in the list, not the view model itself. Unfortunately, Silverlight does not support (to my knowledge) the relative binding syntax available in WPF, so it can be troublesome to try to bind the click in the grid to a command.

If there is a simple way to do this I'd love to learn more, but any examples I've seen were fairly complex to maintain the "purity" of leaving everything out of the code behind. Me? I'm a bit more pragmatic. I'd rather go ahead and bind the click event (I know, some of you are shuddering ... that means a code behind ...) like I did above. However, there is a compromise!

Instead of making my view aware of the event aggregator service, I can do a bit better. The view model already knows about the service. Let's assume we could bind directly to a command. We'll create a command like this, in the view model:

public DelegateCommand<MyEntity> DeleteCommand { get; set; }

We can then wire the command to publication of a delete event:

DeleteCommand = new DelegateCommand<MyEntity>(
                _service.GetEventService().GetEvent<DeleteMyEntityEvent>().Publish, o => true);

Notice we don't supply parameters to Publish. That's because our event is already wired to act on an instance of MyEntity, and so the event will fire and pass that entity as a parameter.

Now we can clean up our code behind to look like this:

private void Button_Delete_Click(object sender, System.Windows.RoutedEventArgs e)
{
    Button src = e.OriginalSource as Button; 
    if (src != null)
    {
        MyEntity entity = src.DataContext as MyEntity;
        if (entity != null)
        {
            if (_ViewModel.DeleteCommand.CanExecute(entity))
            {
                _ViewModel.DeleteCommand.Execute(entity);
            }
        }
    }
}

So, it's not "pure" in the sense that I do end up with code behind ... but ask me, do I really care? Is it all that bad? The view is aware of the view model, so why not allow it to response to what views do well (view events) and marshall them to the view model? The implementation of the command is still hidden from view.

With some clever attached properties, we could also make the button disable if CanExecute is false, and because of data virtualization we'd only be evaluating it for the grid items that are in the view.

Jeremy Likness

1 comment:

  1. You can use a DataContextProxy to get to your parent viewmodel from your listitem:

    http://weblogs.asp.net/dwahlin/archive/2009/08/20/creating-a-silverlight-datacontext-proxy-to-simplify-data-binding-in-nested-controls.aspx

    You might also be interested in this relativesource binding helper:
    http://www.scottlogic.co.uk/blog/wpf/2009/02/relativesource-binding-in-silverlight/

    ReplyDelete