MVVM: Behandeln von Events über Commands, Teil 1
Durch WPF ist eine neue Softwarearchitektur sehr in Mode gekommen: MVVM (Model – View – ViewModel; http://en.wikipedia.org/wiki/Model_View_ViewModel). Dank Databinding und Commanding ist diese Softwarearchitektur in WPF relativ einfach umsetzbar. Ein Problem, dass es jedoch nach wie vor gibt, ist die oft fehlende Command-Property bei diversen WPF Controls bzw. die fehlende Möglichkeit, Commands aufgrund von Events aufzurufen.
Ein Beispiel?
Angenommen wir haben eine View, die eine ListView verwendet. Diese ListView stellt einen Event namens “SelectionChanged” zur Verfügung. Wir haben über WPF nun keine Standard-Lösung, wie wir diesen Event auf ein Command einer ViewModel Klasse leiten. Also, wie wir ein Command aufrufen, sobald der Event der ListView gefeuert wird/wurde.
Abhilfe über Attached Properties
Die Lösung liegt in den von WPF eingeführten Attached Properties (http://msdn.microsoft.com/en-us/library/ms749011.aspx).
Wir wollen also erreichen, das ein Command ausgefürt wird, sobald eine Event gefeuert wird. Wir erstellen uns zuerst eine Klasse, die zwei unterschiedliche Attached Properties beinhaltet: Command und EventName. Command ist das Command, das ausgeführt werden soll, sobald der Event gefeuert wird. EventName ist der Name des Events, auf den wir horchen wollen — also der Event, der das Command triggern soll. Wichtig dabei ist, dass wir Callback-Methoden registrieren, die aufgerufen werden, sobald die Attached Property gesetzt wird. In diesen Eventhandler registrieren wir über .NET Reflection eine anonyme Methode als Eventhandler für den Event, der über die Attached Property angegeben wurde:
public class AttachedCommand : DependencyObject
{
private static IDictionary<DependencyObject, CommandParameter> Parameter { get; set; }
public static ICommand GetCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(AttachedCommand), new UIPropertyMetadata(CommandChanged));
public static string GetEventName(DependencyObject obj)
{
return (string)obj.GetValue(EventNameProperty);
}
public static void SetEventName(DependencyObject obj, string value)
{
obj.SetValue(EventNameProperty, value);
}
public static readonly DependencyProperty EventNameProperty =
DependencyProperty.RegisterAttached("EventName", typeof(string), typeof(AttachedCommand), new UIPropertyMetadata(EventNameChanged));
public static void CommandChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
if (!Parameter.ContainsKey(dependencyObject)) Parameter.Add(dependencyObject, CommandParameter.Default);
Parameter[dependencyObject].Command = args.NewValue as ICommand;
_AttachEventHandler(dependencyObject);
}
public static void EventNameChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
if (!Parameter.ContainsKey(dependencyObject)) Parameter.Add(dependencyObject, CommandParameter.Default);
Parameter[dependencyObject].EventName = args.NewValue as string;
_AttachEventHandler(dependencyObject);
}
private static void _AttachEventHandler(DependencyObject dependencyObject)
{
if (dependencyObject == null) throw new ArgumentNullException("DependencyObject is null");
if (!Parameter.ContainsKey(dependencyObject)) return;
CommandParameter parameter = Parameter[dependencyObject];
if (parameter.Command == null || string.IsNullOrEmpty(parameter.EventName)) return;
EventInfo eventInfo = dependencyObject.GetType().GetEvent(parameter.EventName);
if (eventInfo == null)
throw new InvalidProgramException(string.Format("Cannot find am event with the name <{0}>.", parameter.EventName));
parameter.Callback = _CreateHandler(eventInfo, () => { parameter.Command.Execute(null); });
eventInfo.AddEventHandler(dependencyObject, parameter.Callback);
}
}
Parameter speichert eine Liste von CommandParameter, die notwendige Metadaten für das Commanding speichert. Wir brauchen diese Liste deshalb, weil wir die Attached Properties ja auf unterschiedlichen WPF Controls verwenden können. In der _AttachEventHandler Methode holen wir uns den Event über Reflection und speichern einen neuen Eventhandler für den Event, der schlussendlich das Command ausführt.
In der View können wir diese Attached Properties jetzt ganz einfach verwenden, in dem wir zuerst den Namespace einbinden und anschließend auf einem beliebigen Controls die Properties anwenden:
<Window x:Class="CommandAction.Views.Master"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:commanding="clr-namespace:CommandAction.Commanding"
xmlns:viewmodels="clr-namespace:CommandAction.ViewModels"
Title="Master" Height="306" Width="722"
WindowStyle="ToolWindow" WindowStartupLocation="CenterScreen">
<Window.DataContext>
<viewmodels:MasterViewModel />
</Window.DataContext>
<Border Margin="50" CornerRadius="20" Background="YellowGreen"
commanding:AttachedCommand.Command="{Binding MouseLeaveCommand}" commanding:AttachedCommand.EventName="MouseLeave">
</Border>
</Window>
Problem?
Soweit so gut. Das einzige Problem das wir über diese Methode bekommen ist, dass wir für ein WPF Control nur einen Event auf ein Command umleiten können, weil für die Attached Properties nur einmal auf ein WPF Control anwenden können. Wollen wir mehrere Events auf Commands umleiten, müssen wir uns einen anderen Weg einfallen lassen. Angenommen wir wollen für ein Image Control den MouseEnter und MouseLeave Event über zwei unterschiedliche Commands behandeln, können wir das mit der Attached Property Variante nicht umsetzen (außer wir wrappen rund um das Image Control ein anderes Control).
Lösen können wir das mit den seit Expression Blend 3 zur Verfügung stehenden Behaviors/Triggers. Wie das ganze funktioniert, erklär ich in meinem nächsten Post, der in den nächsten Tagen kommen wird.
Das Demoprojekt gibts wie immer unter http://downloads.juergenoberngruber.at/blog/CommandAction.zip zu finden.
In diesem Sinne.
