In my previous post I showed how to write a simple mark up extension. I also noticed that not too many folks are aware of the fact that you can obtain a lot of information about how markup extension is used in XAML from the provider object. I am going to demonstrate how to do this. I will write an extension that would invoke a method on view model when a button is clicked by “binding” directly to Click event of the button.
Frist, we have to know more about the object we are using mark up extension on. I am going to call mine EventToMethod. Here is what the class looks like
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
using System.Xaml;
namespace SL5Features.Extensions
{
/// <summary>
/// Extension that invokes a specified method when an event occurs.
/// </summary>
public class EventToMethod : FrameworkElement, IMarkupExtension<Delegate>
{
/// <summary>
/// method handle that will be invoked
/// </summary>
private MethodInfo executeMethod = null;
/// <summary>
/// Main method required to be implemented by mark up extension
/// </summary>
/// <param name="serviceProvider">Service provider</param>
/// <returns>Event delegate</returns>
public Delegate ProvideValue(IServiceProvider serviceProvider)
{
// obtain value target provider and get mark up extension’s target from it
var target =
(IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
// since we are bound to an event, we are getting EventInfo object
EventInfo prop = target.TargetProperty as EventInfo;
// test to see what type of event we are getting
if (prop.EventHandlerType.Equals(typeof(RoutedEventHandler)))
{
// create delegate for routed event and return it
return new RoutedEventHandler(RoutedHandler);
}
else
{
//create delegate for regular event and return it
return new EventHandler(RegularHandler);
}
}
/// <summary>
/// Routed event handler delegate to invoke in response to routed event
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void RoutedHandler(object sender, RoutedEventArgs args)
{
RegularHandler(sender, args);
}
/// <summary>
/// Event handler to invoke in response to plain vanilla event
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void RegularHandler(object sender, EventArgs args)
{
if (executeMethod == null)
{
Type type = ViewModel.GetType();
executeMethod = type.GetMethod(MethodName);
}
executeMethod.Invoke(ViewModel, new object[] { });
}
/// <summary>
/// Name of the method to invoke when an event is invoked, such as button click
/// </summary>
public string MethodName
{
get { return (string)GetValue(MethodNameProperty); }
set { SetValue(MethodNameProperty, value); }
}
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register(
"MethodName",
typeof(string),
typeof(EventToMethod),
new PropertyMetadata(null));
/// <summary>
/// View model to invoke
/// </summary>
public Object ViewModel
{
get { return (Object)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(
"ViewModel", typeof(Object),
typeof(EventToMethod),
new PropertyMetadata(null));
}
}
I documented properties, etc… The basic idea is to get button (or any other) object from the provider, get event information, then create a delegate for that event and return it, just like any other mark up extension must return a value. I added ViewModel and MethodName properties so that I can get event handle from ViewModel based on event name. If you bind your mark up extension to a property on a control, TargetProperty will contain PropertyInfo object.
XAML could not look simpler:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:SL5Features.Models"
xmlns:ext="clr-namespace:SL5Features.Extensions">
<DataTemplate DataType="models:Person">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="7"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="7"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="First Name:"
Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="Red"/>
<TextBlock Text="Last Name:"
Grid.Column="0" Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="Red"/>
<TextBox Text="{Binding Path=FirstName, Mode=TwoWay}"
Grid.Column="2" Grid.Row="0"
Background="Yellow"/>
<TextBox Text="{Binding Path=LastName, Mode=TwoWay}"
Grid.Column="2" Grid.Row="2"
Background="Yellow"/>
<Button Content="Save" HorizontalAlignment="Left"
Grid.Row="4" Grid.Column="0"
Click="{ext:EventToMethod
ViewModel={Binding ElementName=LayoutRoot, Path=DataContext},
MethodName=RunIt}"/>
</Grid>
</DataTemplate>
</ResourceDictionary>
As you can see, I am binding button’s click method via my mark up extension. Now, in my view model I just need to declare RunIt method, and that is it:
using SL5Features.Models;
using System.Windows;
namespace SL5Features.ViewModels
{
public class PersonViewModel : ViewModelBase<Person>
{
public PersonViewModel()
{
Model = new Person() { FirstName = "Sergey", LastName = "Barskiy" };
}
public void Run(object parameter)
{
MessageBox.Show("Run");
}
public void RunIt()
{
MessageBox.Show("RunIt");
}
public bool CanRun(object parameter)
{
return true;
}
}
}
I added all the functionality to the solution from the previous post, but I am using alternate template for this implementation. Too see the end result, just add ?Template=Alt to the query string when you run the app.
To summarize, the idea is the same as my previous post – eliminate the need to create commands in my view model.
You can download updated solution here.
Thanks.
Is any chance to not explicitly specify ViewModel and get it from DataContext ?
Not sure what you mean, Cesnek. THe extension does get the model from datacontext