When the Silverlight 5 beta shipped, one of the things that I wrote about was trying to use a markup extension in order to link between something like a Click event on a Button and a method to be invoked on the ViewModel rather than on the view.
And so if I have a ViewModel like this;
public class ViewModel { public void SomeMethod() { } }
then I want to be able to build a view something like this;
<UserControl x:Class="SilverlightApplication11.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" xmlns:local="clr-namespace:SilverlightApplication11"> <UserControl.DataContext> <local:ViewModel /> </UserControl.DataContext> <Grid x:Name="LayoutRoot" Background="White"> <Button Click="{local:Invoke MethodName=SomeMethod,Source={Binding}}" /> </Grid> </UserControl>
and have a MarkupExtension magically link up the button’s Click event to the method on the ViewModel.
Note that this is far from essential as it’s something you can do with Expression Blend’s InvokeMethodAction linked to a Trigger or you can do it with other MVVM frameworks so my attempt here is mostly an experiment.
I came up with an extension which, in its first iteration looks like this;
public class InvokeExtension : MarkupExtension { class BindingHandler { private BindingHandler() { } public static BindingHandler ForRegularSource(object source, string methodName) { BindingHandler handler = new BindingHandler(); handler.dataContext = source; handler.dataContextMethodName = methodName; handler.GetMethodInfo(); return(handler); } public static BindingHandler ForBindingSource(FrameworkElement element, string methodName) { BindingHandler handler = new BindingHandler(); handler.dataContextMethodName = methodName; element.DataContextChanged += handler.OnContextChanged; return(handler); } void OnContextChanged(object sender, DependencyPropertyChangedEventArgs e) { this.dataContext = e.NewValue; this.dataContextMethodInfo = null; GetMethodInfo(); } public void Handler(object sender, EventArgs args) { if (this.dataContextMethodInfo != null) { object[] argList = null; ParameterInfo[] paramInfo = this.dataContextMethodInfo.GetParameters(); if (paramInfo.Length == 1) { argList = new object[] { sender }; } else if (paramInfo.Length == 2) { argList = new object[] { sender, args }; } this.dataContextMethodInfo.Invoke(this.dataContext, argList); } } void GetMethodInfo() { if ((this.dataContext != null) && (this.dataContextMethodInfo == null)) { this.dataContextMethodInfo = this.dataContext.GetType().GetMethod( this.dataContextMethodName, BindingFlags.Public | BindingFlags.Instance); if (this.dataContextMethodInfo == null) { throw new ArgumentException( string.Format("Cannot find method named {0} on type {1}", this.dataContextMethodName, this.dataContext.GetType().ToString())); } } } public static MethodInfo HandlerMethodInfo { get { return (typeof(BindingHandler).GetMethod("Handler")); } } object dataContext; MethodInfo dataContextMethodInfo; string dataContextMethodName; } public string MethodName { get; set; } public object Source { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { Delegate returnValue = null; if (string.IsNullOrEmpty(this.MethodName)) { throw new ArgumentException("method name is required"); } if (this.Source == null) { throw new ArgumentException("source object is required"); } IProvideValueTarget ipvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget; if ((ipvt.TargetObject != null) && (ipvt.TargetProperty != null)) { EventInfo targetEvent = ipvt.TargetProperty as EventInfo; if (targetEvent != null) { BindingHandler handler = null; if (!(this.Source is Binding)) { handler = BindingHandler.ForRegularSource(this.Source, this.MethodName); } else if (ipvt.TargetObject is FrameworkElement) { handler = BindingHandler.ForBindingSource( ipvt.TargetObject as FrameworkElement, this.MethodName); } if (handler != null) { returnValue = Delegate.CreateDelegate( targetEvent.EventHandlerType, handler, BindingHandler.HandlerMethodInfo); } else { throw new InvalidOperationException("Expected object or binding source"); } } } return (returnValue); } }
and so the idea is that this can work as below;
<UserControl x:Class="SilverlightApplication11.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" xmlns:local="clr-namespace:SilverlightApplication11"> <UserControl.Resources> <local:ViewModel x:Key="myData" /> </UserControl.Resources> <StackPanel x:Name="LayoutRoot" Background="White"> <Button Content="One" Click="{local:Invoke MethodName=Method1,Source={StaticResource myData}}" /> <Button Content="Two" Click="{local:Invoke MethodName=Method2,Source={StaticResource myData}}" /> <Button Content="Three" Click="{local:Invoke MethodName=Method3,Source={StaticResource myData}}" /> <Button Content="Four" Click="{local:Invoke MethodName=Method4,Source={StaticResource myData}}" /> <Button Content="Five" Click="{local:Invoke MethodName=Method5,Source={StaticResource myData}}" /> </StackPanel> </UserControl>
where I’m linking the view to the viewmodel by using the StaticResource markup extension and the view model has methods on it as below;
public class ViewModel { public void Method1() { } public void Method2(object sender) { } public void Method3(object sender, EventArgs args) { } public void Method4(Button sender, EventArgs args) { } public void Method5(Button sender, RoutedEventArgs args) { } }
but, equally, it can work (at least in the simple case) where I’m using the Binding markup extension to link the view to the viewmodel which is a lightly different game because the data-context is not resolved at the time that the markup extension is invoked and may change during the lifetime of the UI;
<UserControl x:Class="SilverlightApplication11.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" xmlns:local="clr-namespace:SilverlightApplication11"> <UserControl.DataContext> <local:ViewModel/> </UserControl.DataContext> <StackPanel x:Name="LayoutRoot" Background="White"> <Button Content="One" Click="{local:Invoke MethodName=Method1,Source={Binding}}" /> <Button Content="Two" Click="{local:Invoke MethodName=Method2,Source={Binding}}" /> <Button Content="Three" Click="{local:Invoke MethodName=Method3,Source={Binding}}" /> <Button Content="Four" Click="{local:Invoke MethodName=Method4,Source={Binding}}" /> <Button Content="Five" Click="{local:Invoke MethodName=Method5,Source={Binding}}" /> </StackPanel> </UserControl>
where the method that we’re trying to invoke isn’t available to be resolved at the time that the markup extension runs so we have to defer it until the DataContextChanged event fires at a later point.
I’m sure there’s a bunch of things that an extension like this should do in order to be fully functioning but that’s as far as I wanted to take it after the previous post where I didn’t have DataContextChanged and so couldn’t get this to work.
The code for this post is available for download here.
Update – while the code above hangs together for the case I used it for, I got a mail from someone who was using it in a more specific case where they wanted to use RelativeSource as part of their bindings and I don’t think that would work and I’m not sure that ElementName style binding would either.
I didn’t spend very long on this but I changed the approach from waiting for a DataContextChanged event to using some attached properties that sync up to bindings. It’s had very little testing but I include it here for completeness as an alternative implementation of InvokeExtension from above;
using System; using System.Reflection; using System.Windows; using System.Windows.Data; using System.Windows.Markup; namespace SilverlightApplication11 { public class InvokeExtension : MarkupExtension { class BindingHandler : DependencyObject { private BindingHandler() { } public static BindingHandler ForRegularSource(object source, string methodName) { BindingHandler handler = new BindingHandler(); handler.dataContext = source; handler.dataContextMethodName = methodName; handler.GetMethodInfo(); return (handler); } public static BindingHandler ForBindingSource(FrameworkElement element, Binding binding, string methodName) { BindingHandler handler = new BindingHandler(); handler.dataContextMethodName = methodName; element.SetValue(BindingHandlerProperty, handler); element.SetBinding(BindingHandlerSourceProperty, binding); return (handler); } public void Handler(object sender, EventArgs args) { if (this.dataContextMethodInfo != null) { object[] argList = null; ParameterInfo[] paramInfo = this.dataContextMethodInfo.GetParameters(); if (paramInfo.Length == 1) { argList = new object[] { sender }; } else if (paramInfo.Length == 2) { argList = new object[] { sender, args }; } this.dataContextMethodInfo.Invoke(this.dataContext, argList); } } void GetMethodInfo() { if ((this.dataContext != null) && (this.dataContextMethodInfo == null)) { this.dataContextMethodInfo = this.dataContext.GetType().GetMethod( this.dataContextMethodName, BindingFlags.Public | BindingFlags.Instance); if (this.dataContextMethodInfo == null) { throw new ArgumentException( string.Format("Cannot find method named {0} on type {1}", this.dataContextMethodName, this.dataContext.GetType().ToString())); } } } public static MethodInfo HandlerMethodInfo { get { return (typeof(BindingHandler).GetMethod("Handler")); } } public static DependencyProperty BindingHandlerSourceProperty = DependencyProperty.RegisterAttached("BindingHandlerSource", typeof(object), typeof(BindingHandler), new PropertyMetadata(null, SourceChanged)); public static DependencyProperty BindingHandlerProperty = DependencyProperty.RegisterAttached("BindingHandler", typeof(object), typeof(BindingHandler), null); public static void SourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { BindingHandler handler = (BindingHandler)sender.GetValue(BindingHandlerProperty); if (handler != null) { handler.dataContext = args.NewValue; handler.dataContextMethodInfo = null; handler.GetMethodInfo(); } } object dataContext; MethodInfo dataContextMethodInfo; string dataContextMethodName; } public string MethodName { get; set; } public object Source { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { Delegate returnValue = null; if (string.IsNullOrEmpty(this.MethodName)) { throw new ArgumentException("method name is required"); } if (this.Source == null) { throw new ArgumentException("source object is required"); } IProvideValueTarget ipvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget; if ((ipvt.TargetObject != null) && (ipvt.TargetProperty != null)) { EventInfo targetEvent = ipvt.TargetProperty as EventInfo; if (targetEvent != null) { BindingHandler handler = null; if (!(this.Source is Binding)) { handler = BindingHandler.ForRegularSource(this.Source, this.MethodName); } else if (ipvt.TargetObject is DependencyObject) { handler = BindingHandler.ForBindingSource( (FrameworkElement)ipvt.TargetObject, (Binding)this.Source, this.MethodName); } if (handler != null) { returnValue = Delegate.CreateDelegate( targetEvent.EventHandlerType, handler, BindingHandler.HandlerMethodInfo); } else { throw new InvalidOperationException("Expected object or binding source"); } } } return (returnValue); } } }