In my last post, I posted an implementation of the ViewModel or M-V-VM pattern for use in Silverlight applications. This pattern allows you to decouple your view presentation logic and data from the view, thereby facilitating independent development/design of your app as well as easier unit testing of your application code. I will highly recommend you check out that post for the more detailed description and intro sample if you haven't already done so, as this post will build further on that.
Now that I've done some additional exploration, and prototyping, I think the syntax used to define the glue between the View and the ViewModel as first presented was a bit suboptimal, and could be re-done to be much more intuitive. Read on to find out more.
Amazon Search Sample
In that post, I had a simple Amazon Search app to demonstrate the view model pattern. Focusing on the search aspect of the scenario, here are the relevant snippets of code and XAML forming the view model and the view respectively:
public
class SearchViewModel : Model {
...
publicvoid Search(string keyword) {
...
}
}
<vm:Viewxmlns="..."xmlns:x="..."xmlns:vm="clr-namespace:Silverlight.FX.ViewModel;assembly=Silverlight.FX"xmlns:app="clr-namespace:AmazonSearch.Views"><vm:View.Model><app:SearchViewModel/></vm:View.Model>
...
<ButtonContent="Search"IsEnabled="{Binding CanSearch}"><vm:ButtonEvents.Click><vm:InvokeMethodMethodName="Search"><vm:ElementParameterElementName="searchTextBox"ElementProperty="Text"/></vm:InvokeMethod></vm:ButtonEvents.Click></Button>
...
</vm:View>
Action behaviors were used to implement the glue that allows a View to invoke some operation defined on the View Model in response to an event. As you can see in the example above, an InvokeMethod action has been associated with the Button's Click event and it is set up to invoke the Search method on the ViewModel passing in the Text in the specified TextBox.
InvokeMethod Shortcomings in Associating View to ViewModel
The first issue with this approach is that it isn't super-friendly. The XAML to define a list of parameters gets verbose very quickly. Secondly it doesn't allow for a number of scenarios, around passing in arbitrary data into the view model. I noted in the post that I might want to do something like this instead:
<
Button
Content
="Search"
Click
="{Action InvokeMethod Search(searchTextBox.Text)}"
IsEnabled
="{Binding CanSearch}"
/>
I got to thinking about this after writing and re-reading the post myself, and what occurred to me is that this is quite a slippery slope, eventually leading to a full expression language. Not wanting to create such a thing, I started reflecting on what could be repurposed.
I realized the Dynamic Language Runtime (DLR) would be a perfect fit. It is just the right designer-friendly glue that can be used to call into the bulk of the logic that is itself developed using statically compiled code that exists in the view model and the rest of the app. I could scope the use of the script down to a minimum by hosting the DLR to execute just script expressions. The DLR would also give me the benefit of using something like JavaScript, and potentially on any another scripting engine created to run on the DLR.
DLR-based InvokeScript
So I got to work, and created an InvokeScript action (leveraging the extensibility of actions), and got this working:
<
Button
Content
="Search"
IsEnabled
="{Binding CanSearch}"
>
<
vm:ButtonEvents.Click
>
<
vm:InvokeScript
Script
="$model.Search(searchTextBox.Text)"
/>
</
vm:ButtonEvents.Click
>
</
Button
>
The verbose XAML for defining parameters was gone, and I didn't have to create my own mini-language. However this is still too much XAML for me. With a bit more work, I was able to get the following syntax to work:
<
Button
Content
="Search"
vm:ButtonEvents.Click="$model.Search(searchTextBox.Text)"
/>
The InvokeScript does its work by hosting the DLR. As a DLR host the InvokeScript action is able to resolve things like $model to the current ViewModel instance. It also resolves "searchTextBox" when the script engine hits that. To do so, it first looks to see if it's a property of the Button (which it isn't, so it continues to search). Next it looks to see if it can find it in the visual tree using FindName or in the resources dictionary via a recursive FindResource (and it does find it in the tree). The bottom line is the script expression is simple and intuitive. And the DLR host does all the work through an intuitive set of rules, rather than have those be spelt out in a verbose form in XAML.This has me pretty satisfied.
As an aside, the script expression can also reference a variable named $event that represents the current event. This is useful if there is some interesting data in the event argument that you want to send across to the ViewModel operation (eg. mouse coordinates in a mouse event) or some property on the event argument you want to set by using the return value of the operation itself (eg. Canceled on a CancelEventArgs).
Essentially the InvokeScript action creates a little data flow pipe as shown below that flows event information and pieces of data from the View into the operation in the ViewModel. The pipe is also able to flow back the return value from the operation for use in the View, even though it hasn't been depicted here.
You might be wondering how the script engine is instantiated. This gives me a chance to introduce my framework's derived Application type that is used in App.xaml - XApplication. Amongst various other pieces of functionality, XApplication provides the semantics of being a global service container. The application developer can write this markup in App.xaml to declaratively define the instance of a ScriptService that will be used in their application. At runtime, the InvokeScript action looks for the current XApplication instance, and queries for the ScriptService service and uses the ScriptEngine that the service is responsible for instantiating and managing based on the specified language.
<
fxa:XApplication
xmlns
="..."
xmlns:x
="..."
xmlns:fxa
="clr-namespace:Silverlight.FX.Applications;assembly=Silverlight.FX"
>
<
fxa:XApplication.Services
>
<
fxa:ScriptService
ScriptLanguage
="JavaScript"
/>
</
fxa:XApplication.Services
>
</
fxa:XApplication
>
Note that you'll need to package the DLR assemblies (Microsoft.Scripting.*.dll) and the language specific ones (such as Microsoft.Jscript.*.dll for JavaScript) into your application's xap package. These are part of the Silverlight 2 SDK. Just add a reference to those dlls in your application project in Visual Studio and the XAP packager will do the right thing to include them in as part of the build process.
Code and Summary
As usual you can download the all of the code, sample, and framework to play with this to your heart's content. Do you think the use of DLR here helps signficantly simplify the view model pattern?
No comments yet.
You must be logged in to add your own comment.