(Note: since this post, I have posted a Version 2 of the MethodCaller solution. Click here for a description of the changes and the latest source code.)
The Problem
I’ve become a huge fan of the MVVM pattern for development with WPF. I’m definitely still learning the finer points of this pattern. I’ve been using it now in my private projects for some time now and one thing that I have bumped up against in several places is the issue of how to trigger View/UI code based on ViewModel events or changes. This becomes especially important as I try to really tweak the user experience so that the movement through the application is very fluid.
Of course, databinding makes it a cinch to bind View and ViewModel properties together. And that covers the vast portion of issues related to reflecting VM changes in the View. But there seem to be some cases where some code in the View or UI needs to be executed based on ViewModel changes. Take the following example:
Let’s say I have an ItemsControl housed in a ScrollView bound to a PersonsViewModel that exposes a BindingList of PersonViewModel’s. The PersonViewModel exposes an IsSelected property that the View’s DataTemplate uses to visually indicate the item’s selected state like this:
<DataTemplate DataType="{x:Type local:PersonViewModel}">
<Label x:Name="uxRow" Content="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="uxRow" Property="Background" Value="LightBlue" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Now, if the underlying PersonViewModel can become selected in some way other than the user clicking on the item in the ItemsControl, there is a good chance that the item may currently be outside the ScrollViewer’s viewing area. So somehow we need to call BringIntoView() on the Label to cause the ScrollView to scroll to the selected item.
The big question is, how? Being as we’re talking about items generated at run-time and we’re dealing item containers that may or may not even be created yet, it’s a bit of a problem to somehow link up the ViewModel “event” of an item becoming selected to a UI method on a specific UI container like Label.BringIntoView().
I did quite a bit of searching and was unable to find any solutions to this problem. In fact, I was unable to find even any mentions of this problem on the web, so that left me wondering if I was doing something fundamentally wrong in the structure of my application and the fact that I was running into this problem at all was a warning flag signalling poor design. (If someone out there has some input on this question, please speak up!)
However, any way I looked at it, it seems like for a really polished UX there are cases where some UI code needs to be executed based on changes at the VM level where simple property-to-property bindings don’t solve the problem.
The Proposed Solution
After some hacking about, I created a solution using attached behaviors that seems to work quite well for my purposes. I must credit Mike Hillberg and his MethodCommand solution and Marlon Grech and his Attached Command Behaviors solution for guidance and inspiration as I waded into this.
So using the same example given above, here’s what the XAML implementing my solution looks like:
<DataTemplate DataType="{x:Type local:PersonViewModel}">
<Label x:Name="uxRow" Content="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="uxRow" Property="Background" Value="LightBlue" />
<Setter TargetName="uxRow" Property="mc:MethodCaller.MethodName" Value="BringIntoView" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Notice the second <Setter>. When IsSelected becomes true on the underlying PersonViewModel, the string “BringIntoView” is sent to the MethodCaller.MethodName attached property. That custom property invokes the specified method name on the underlying Dependency Object, which is the Label. That’s pretty simple and painless.
So, this is fine and dandy, but what if the method I need to call requires some arguments? Well, that requires a slightly more advanced solution, but it’s not that difficult. Here’s what that solution looks like:
<Setter Property="mc:MethodCaller.MethodCall">
<Setter.Value>
<mc:MethodCall MethodName="SomeMethod">
<mc:MethodCall.Arguments>
<mc:MethodArgument Value="Some argument" />
<mc:MethodArgument>
<clr:Int32>100</clr:Int32>
</mc:MethodArgument>
<mc:MethodArgument Value="{Binding PropertyName}" />
</mc:MethodCall.Arguments>
</mc:MethodCall>
</Setter.Value>
</Setter>
So here we’re using a different attached property, MethodCaller.MethodCall, which takes an object called MethodCall. This object contains the MethodName and a collection of Arguments to pass to the invoked method. Additionally, it also has a Target property (not shown above) that allows you to specify the object on which to invoke the method if it something other than the dependency object on which the setter is acting. Both the Target property and the MethodArgument.Value property support data binding.
There is one more extension to this functionality that I’ve created that comes in handy: the ability to execute a method in response to an Event, instead of in response to a property change as shown above in the DataTrigger. Here is an example of what that solution looks like:
<Label Focusable="True" x:Name="uxRow" Content="{Binding Name}">
<mc:MethodCaller.OnEventMethodCall>
<mc:OnEventMethodCall EventName="Selected" MethodName="BringIntoView"
EventSource="{Binding}" />
</mc:MethodCaller.OnEventMethodCall>
</Label>
You can see here, we’re using a third attached property, MethodCaller.OnEventMethodCall, that takes an OnEventMethodCall object. The OnEventMethodCall object is a subclass of MethodCall and adds two new properties: EventName and EventSource (which also supports binding). The OnEventMethodCall object adds a handler to the specified event on the event source (which defaults to the underlying dependency object) and when the event fires, it invokes the specified method.
So in the XAML above, we’re subscribing to the Selected event on the underlying data context (a PersonViewModel referenced by {Binding}). When that event fires, we’re executing the BringIntoView method on the Label. This accomplishes the same goal as the example above that uses the DataTrigger method to listen for a change in IsSelected.
Additionally, this event handling solution can work the other way: it can handle a UI event and invoke a ViewModel method, as in the example below:
<TextBox x:Name=”uxText”>
<mc:MethodCaller.OnEventMethodCall>
<mc:OnEventMethodCall EventName=”TextChanged” Target=”{Binding}” MethodName=”FindPerson”>
<mc:MethodCall.Arguments>
<mc:MethodArgument Value=”{Binding Text, ElementName=uxText}” />
</mc:MethodCall.Arguments>
</mc:OnEventMethodCall>
</mc:MethodCaller.OnEventMethodCall>
</TextBox>
In the example XAML above, when the TextChanged event fires on the TextBox, the FindPerson method is called on the underlying data object (in this case, the PersonsViewModel) and the TextBox.Text is passed in as an argument.
Known Issues
One limitation to the present solution is that only one MethodCaller.OnEventMethodCall property can be set per Dependency Object. So that limits you from being able to set up event handlers on multiple events for the same object. This is something that I plan on solving by exposing an attached property that takes a collection of OnEventMethodCall objects. (Version 2 of the solution adds support for multiple events.)
Also, OnEventMethodCall can only handle events that follow the standard signature of EventName(ByVal sender As Object, ByVal e As EventArgs). (The e parameter can be any subclass of EventArgs, of course.)
There are some areas of the attached behavior where we just silently bail if some of the require information is missing (MethodName for example). To be maximally helpful, we should probably throw exceptions in those places to help the developer spot the problem quicker. I may sharpen this area up in a future update.
Source Code and Demo
(Note: since this post, I have posted a Version 2 of the MethodCaller solution. Click here for a description of the changes and the latest source code.)
You can download the source code for the support attached properties and objects, as well as a demo application here.
The demo app demonstrates the following scenarios:
- Invoking a UI method call via DataTrigger
- PersonViewModel.IsSelected -> Label.BringIntoView()
- Invoking a VM method based on a UI event
- Label.MouseLeftButtonDown -> PersonsViewModel.SelectPerson()
- TextBox.TextChanged -> PersonsViewModel.FindPerson()
- Invoking a View method based on a VM event
- PersonsViewModel.FoundPerson -> Window.FoundPerson()
Summary
Feel free to use this code, but use at your own risk. It has not been tested extensively and I’m sure it has some bugs here or there that will come up as different scenarios are used.
Please post any comments, thoughts, criticisms, suggestions, or insight. I’m eager to learn all I can.
—Benjamin