Binding Events to View Model Methods in Windows Store Apps

June 24th 2013 Binding Windows Store

One of the challenges of using MVVM pattern with different UI frameworks that always comes up is how to bind events that are not exposed as commands to the view model. Windows Store apps are no exception to that. In this blog post I'll try to give an overview of the available possibilities one can choose from based on individual requirements and personal preferences.

The simplest approach would be to add an event handler to the view code behind and call the view model method from there. To make this plumbing code simpler it makes sense to add a strongly typed view model property to the view. While this approach keeps the view model isolated from the specifics of the view, it falls short on the goal of having as little code behind in the view as possible. Also the code in the view is not reusable and needs to be repeated for every event.

<ListView IsItemClickEnabled="True" 
          ItemsSource="{Binding Items}" 
          ItemClick="OnItemClick" />
public ViewModel ViewModel
{
    get { return DataContext as ViewModel; }
}

private void OnItemClick(object sender, ItemClickEventArgs e)
{
    ViewModel.ItemClicked(e.ClickedItem as string);
}
public void ItemClicked(string item)
{
    // react to the event
}

This can be avoided by creating an attached property and binding a command to it. Of course, the view model now needs to have a command instead of a method. If you're not using any of the existing MVVM frameworks with its own DelegateCommand implementation you might want install Prism.StoreApps NuGet package to get one.

public static class AttachedProperties
{
    public static DependencyProperty ItemClickCommandProperty = 
        DependencyProperty.RegisterAttached("ItemClickCommand",
            typeof(ICommand),
            typeof(AttachedProperties),
            new PropertyMetadata(null, OnItemClickCommandChanged));

    public static void SetItemClickCommand(DependencyObject target, ICommand value)
    {
        target.SetValue(ItemClickCommandProperty, value);
    }

    public static ICommand GetItemClickCommand(DependencyObject target) 
    {
        return (ICommand)target.GetValue(ItemClickCommandProperty);
    }

    private static void OnItemClickCommandChanged(DependencyObject target, 
        DependencyPropertyChangedEventArgs e)
    {
        var element = target as ListViewBase;
        if (element != null)
        {
            // If we're putting in a new command and there wasn't one already
            // hook the event
            if ((e.NewValue != null) && (e.OldValue == null))
            {
                element.ItemClick += OnItemClick;
            }

            // If we're clearing the command and it wasn't already null
            // unhook the event
            else if ((e.NewValue == null) && (e.OldValue != null))
            {
                element.ItemClick -= OnItemClick;
            }
        }
    }

    static void OnItemClick(object sender, ItemClickEventArgs e)
    {
        GetItemClickCommand(sender as ListViewBase).Execute(e.ClickedItem);
    }
}
<ListView IsItemClickEnabled="True" 
          ItemsSource="{Binding Items}" 
          local:AttachedProperties.ItemClickCommand="{Binding ItemClickedCommand}" />
public ViewModel()
{
    ItemClickedCommand = new DelegateCommand<string>(ItemClicked);
}

public void ItemClicked(string item)
{
    // react to the event
}

An even more elegant solution is to create a behavior instead. In this case you need to install Windows.UI.Interactivity NuGet package to get the necessary base classes. View model can remain unchanged.

<ListView IsItemClickEnabled="True" 
        ItemsSource="{Binding Items}">
    <i:Interaction.Behaviors>
        <local:ItemClickedBehavior 
            ItemClickedCommand="{Binding ItemClickedCommand}" />
    </i:Interaction.Behaviors>
</ListView>
public class ItemClickedBehavior : Behavior<ListViewBase>
{
    public static readonly DependencyProperty ItemClickedCommandProperty = 
        DependencyProperty.Register(
            "ItemClickedCommand",
            typeof(ICommand),
            typeof(ItemClickedBehavior),
            new PropertyMetadata(null));

    public ICommand ItemClickedCommand
    {
        get { return (ICommand)GetValue(ItemClickedCommandProperty); }
        set { SetValue(ItemClickedCommandProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.ItemClick += OnItemClick;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.ItemClick -= OnItemClick;
    }

    private void OnItemClick(object sender, ItemClickEventArgs e)
    {
        ItemClickedCommand.Execute(e.ClickedItem);
    }
}

Windows.UI.Interactivity offers a more generic and simpler approach in the form of InvokeCommandAction. Unfortunately it directly sends event arguments as command parameters, breaking the abstraction between the view and the view model. The approach is still useful when the command doesn't need any parameters from the event.

<ListView IsItemClickEnabled="True" 
          ItemsSource="{Binding Items}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ItemClick">
            <i:InvokeCommandAction Command="{Binding ItemClickedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListView>
public ViewModel()
{
    Items = new List<string> {"Item 1", "Item 2", "Item 3"};
    ItemClickedCommand = new DelegateCommand(ItemClicked);
}

public void ItemClicked()
{
    // react to the event
}

You can also use InvokeCommandAction's CommandParameter property to set or bind a value to the command parameter. There's a catch to it, though. Triggers are not part of the visual tree therefore you can't bind directly to page elements using the ElementName binding syntax. You either need to bind the required element property to the view model property separately and have the command read it from there or use the NameScopeBinding helper from the MVVMHelpers.Metro NuGet package to gain access to other elements.

<Page.Resources>
    <b:NameScopeBinding x:Key="ListView" Source="{Binding ElementName=ListView}" />
</Page.Resources>

<ListView x:Name="ListView"
            SelectionMode="Single"
            IsItemClickEnabled="True" 
            ItemsSource="{Binding Items}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ItemClick">
            <i:InvokeCommandAction Command="{Binding ItemClickedCommand}" 
                                   CommandParameter="{Binding Source.SelectedItem, 
                                       Source={StaticResource ListView}}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListView>

Still, to access values from event arguments without the view model being dependent on the event argument type directly, you'll have to resort to one of the previous approaches.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License