MVVM: Behandeln von Events über Commands, Teil 2
In meinem letzten Eintrag gings darum, wie man Events eines WPF Controls auf Commands eines ViewModels umleiten kann. Verwendet habe ich dort die Attached Properties von WPF. Das Problem dabei war aber, dass man für diese Attached Properties für ein Control nur einmal verwenden kann. Das heißt, dass man zB für ein Border-Control nicht zugleich den MouseEnter- und MouseLeave-Event auf ein Command umleiten kann. Um das mit der Lösung der Attached Properties zu erreichen, wäre es notwendig, einen zusätzlichen Wrapper rund um das Border-Control einzuführen:
<Border Margin="50"
commanding:AttachedCommand.Command="{Binding MouseEnterCommand}"
commanding:AttachedCommand.EventName="MouseEnter">
<Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="YellowGreen" CornerRadius="20"
commanding:AttachedCommand.Command="{Binding MouseLeaveCommand}"
commanding:AttachedCommand.CommandParameter="Border #1"
commanding:AttachedCommand.EventName="MouseLeave">
</Border>
</Border>
Um diesem Umweg zu vermeiden, möchte ich heute einen neuen Ansatz präsentieren, der die seit Expression Blend 3 zur Verfügung stehenden Behaviors und Triggers verwendet.
TargetedTriggerAction aus System.Windows.Interactivity verwenden
Die Idee dahinter ist, einen Trigger zu erstellen, der diverse Dependency Properties implementiert. Diese Properties müssen deshalb Dependency Properties sein, weil wir sonst kein Databinding darauf anwenden können. Eine dieser DP ist vom Typ ICommand — also das Command, das wir ausführen wollen, wenn der Trigger gestartet wird:
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandProperty =
´ DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandTrigger),
new PropertyMetadata(null, OnCommandChanged));
Wichtig bei der Dependency Property ist der Callback OnCommandChanged, der dann aufgerufen wird, wenn der DP ein neuer Wert zugewiesen wird.
Warum ist das wichtig? Bei einer Command-Property, die out-of-the-box bei bereits vorhandenen Controls wie Button vom Framework mitgeliefert wird, wird der Status des Controls abhängig von der CanExecute-Methode des Commands verändert. Also liefert CanExecute false zurück, dann wird der Button, der das Command verwendt, deaktiviert. Genau diese Funktionalität müssen/sollen/wollen wir mit unserem Trigger auch abbilden.
Deshalb registrieren wir uns in der OnCommandChanged Methode auf den CanExecuteChanged Event, den jede Klasse, die ICommand implementiert, selbstverständlich auch implementieren muss. Sobald dieser Event geworden wurde, wollen wir prüfen, ob unser Control, das ein Binding auf das Command besitzt, noch einen gültigen Status besitzt:
private static void OnCommandChanged(DependencyObject action, DependencyPropertyChangedEventArgs e)
{
((CommandTrigger)action)._RefreshCommandHandling((ICommand)e.OldValue, (ICommand)e.NewValue);
}
private void _RefreshCommandHandling(ICommand oldCommand, ICommand newCommand)
{
if (oldCommand != null) oldCommand.CanExecuteChanged -= new EventHandler((sender, args) => _UpdateCanExecute());
if (newCommand != null) newCommand.CanExecuteChanged += new EventHandler((sender, args) => _UpdateCanExecute());
_UpdateCanExecute();
}
Wir bekommen im Eventhandler über die EventArgs den alten und den neuen Wert, den die Dependency Property angenommen hat. Das wir haben die Möglichkeit, uns vom Event des alten Commands (also des alten Wertes der DP) zu entfernen und uns auf den Event des neuen Commands (also der neue Wert der DP) zu registrieren. In der _UpdateCanExecute() Methode prüfen wir schlussendlich, ob das Control, auf dem der Trigger angewendet wird, noch den gültigen Status aufgrund von CanExecute besitzt:
private void _UpdateCanExecute()
{
if (this.Command == null) return;
RoutedCommand command = this.Command as RoutedCommand;
if (command != null) this.IsEnabled = command.CanExecute(this.CommandParameter, this.CommandTarget);
else this.IsEnabled = this.Command.CanExecute(this.CommandParameter);
if (this.Target != null && this.SyncOwnerIsEnabled) this.Target.IsEnabled = this.IsEnabled;
}
Im XAML Code wird dieser Trigger jetzt ganz einfach verwendet, indem wir zuerst den richtigen Namespace einbinden und anschließend den Trigger auf ein beliebiges Control anwenden:
<Border>
<interactivity:Interaction.Triggers>
<interactivity:EventTrigger EventName="MouseEnter">
<commanding:CommandTrigger Command="{Binding MouseEnterCommand}" />
</interactivity:EventTrigger>
<interactivity:EventTrigger EventName="MouseLeave">
<commanding:CommandTrigger Command="{Binding MouseLeaveCommand}" />
</interactivity:EventTrigger>
</interactivity:Interaction.Triggers>
</Border>
Das tolle an diesem Trigger ist jetzt, dass er mehrmals auf ein und dem selben Control angewendet werden kann. Wir können jetzt also mehrere Events über den selber Trigger behandeln und auf Commands weiterleiten.
Das Demoprojekt gibts wie immer unter http://downloads.juergenoberngruber.at/blog/CommandAction_Trigger.zip.
In diesem Sinne.
