|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionThis article examines a technique that makes it easy to use routed commands when the command execution logic resides in ViewModel objects. This approach cuts out the middleman; the code-behind file. It allows your ViewModel objects to be the direct recipient of routed command execution and status query notifications. This article assumes that the reader is already familiar with data binding and templates, routed commands, attached properties, and the Model-View-ViewModel (MVVM) design pattern. BackgroundThere was an interesting thread on the WPF Disciples forum about how to use routed commands in conjunction with the Model-View-ViewModel pattern. After much discussion, and a side conversation with Bill Kempf, I began to understand what the core issue was. Most examples of using WPF with the MVP, MVC, or MVVM patterns involve the use of routed commands. Those commands are accompanied by The BenefitsThere are several distinct benefits in having the routed commands in the View talk directly to the ViewModel. Bypassing the code-behind of the View means the View is that much less coupled to a ViewModel. It also means that the ViewModel is not dependent on the View’s code-behind to properly handle a routed command’s events and delegate those calls off to the correct members on the ViewModel objects. Not only that, but it reduces the amount of coding required to create a View, which is important when working in the Designer-Developer workflow. Prior ArtThere are several existing solutions to this type of problem out there, from Dan Crevier’s CommandModel to Rob Eisenberg’s Caliburn framework. I felt that CommandModel was too complicated and restrictive, while Caliburn was too heavy and broad. I am not saying that they are in any way bad solutions, just not what I wanted for this particular task. My SolutionI thought long and hard about the issue, and decided that the solution is to create a custom The other piece of the puzzle is a small interface called This solution is very lightweight, reusable, and does not rely on the use of reflection at all. Seeing it in ActionBefore we examine how my solution works, let’s first take a look at the demo app, which is available for download at the top of this article. When you run the demo, it looks like this:
<Window
x:Class="VMCommanding.DemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:VMCommanding.View"
xmlns:vm="clr-namespace:VMCommanding.ViewModel"
FontSize="13"
ResizeMode="NoResize"
SizeToContent="WidthAndHeight"
Title="ViewModel Commanding Demo"
WindowStartupLocation="CenterScreen"
>
<Window.DataContext>
<vm:CommunityViewModel />
</Window.DataContext>
<Window.Content>
<view:CommunityView />
</Window.Content>
</Window>
Clearly, there is not much going on in the Window. Let’s now move our attention to the content of the Window; the We will examine the ViewModel classes later, but for now, here is the <UserControl
x:Class="VMCommanding.View.CommunityView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:jas="clr-namespace:VMCommanding"
xmlns:view="clr-namespace:VMCommanding.View"
xmlns:vm="clr-namespace:VMCommanding.ViewModel"
jas:CommandSinkBinding.CommandSink="{Binding}"
>
<UserControl.CommandBindings>
<jas:CommandSinkBinding Command="vm:CommunityViewModel.KillAllMembersCommand" />
</UserControl.CommandBindings>
<DockPanel Margin="4">
<Button
DockPanel.Dock="Bottom"
Command="vm:CommunityViewModel.KillAllMembersCommand"
Content="Kill All"
Margin="0,8,0,0"
/>
<ItemsControl ItemsSource="{Binding People}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:PersonView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</UserControl>
The most relevant parts of that XAML is bold. As you can see, this control’s Each <UserControl
x:Class="VMCommanding.View.PersonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:jas="clr-namespace:VMCommanding"
xmlns:vm="clr-namespace:VMCommanding.ViewModel"
jas:CommandSinkBinding.CommandSink="{Binding}"
>
<UserControl.CommandBindings>
<jas:CommandSinkBinding Command="vm:PersonViewModel.DieCommand" />
<jas:CommandSinkBinding Command="vm:PersonViewModel.SpeakCommand" />
</UserControl.CommandBindings>
<UserControl.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="0,0,6,0" />
<Style.Triggers>
<DataTrigger Binding="{Binding CanDie}" Value="False">
<Setter Property="Foreground" Value="#88000000" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel Margin="2" Orientation="Horizontal">
<TextBlock Text="Name:" FontWeight="Bold" />
<TextBlock Text="{Binding Name}" Width="60" />
<TextBlock Text="Age:" FontWeight="Bold" />
<TextBlock Text="{Binding Age}" Width="40" />
<Button
Command="vm:PersonViewModel.SpeakCommand"
CommandParameter="Howdy partner!"
Content="Speak"
Margin="0,0,6,0"
Width="60"
/>
<Button
Command="vm:PersonViewModel.DieCommand"
Content="Die"
Width="60"
/>
</StackPanel>
</UserControl>
Aside from having more stylistic resources and visual elements, this control is essentially the same as the Once again, it is important to note that the code-behind file of the How it WorksNow it is time to turn our attention to how this works. You do not have to read this section of the article in order to use my solution, though I strongly recommend you do for the sake of understanding. ICommandSinkFirst, we will examine the /// <summary>
/// Represents an object that is capable of being notified of
/// a routed command execution by a CommandSinkBinding. This
/// interface is intended to be implemented by a ViewModel class
/// that honors a set of routed commands.
/// </summary>
public interface ICommandSink
{
bool CanExecuteCommand(ICommand command, object parameter, out bool handled);
void ExecuteCommand(ICommand command, object parameter, out bool handled);
}
This is similar to the standard CommandSinkBindingThe real meat is in the /// <summary>
/// A CommandBinding subclass that will attach its
/// CanExecute and Executed events to the event handling
/// methods on the object referenced by its CommandSink property.
/// Set the attached CommandSink property on the element
/// whose CommandBindings collection contain CommandSinkBindings.
/// If you dynamically create an instance of this class and add it
/// to the CommandBindings of an element, you must explicitly set
/// its CommandSink property.
/// </summary>
public class CommandSinkBinding : CommandBinding
{
ICommandSink _commandSink;
public ICommandSink CommandSink
{
get { return _commandSink; }
set
{
if (value == null)
throw new ArgumentNullException("...");
if (_commandSink != null)
throw new InvalidOperationException("...");
_commandSink = value;
base.CanExecute += (s, e) =>
{
bool handled;
e.CanExecute = _commandSink.CanExecuteCommand(
e.Command, e.Parameter, out handled);
e.Handled = handled;
};
base.Executed += (s, e) =>
{
bool handled;
_commandSink.ExecuteCommand(
e.Command, e.Parameter, out handled);
e.Handled = handled;
};
}
}
// Other members omitted for clarity...
}
Now we will turn out attention to the attached public static ICommandSink GetCommandSink(DependencyObject obj)
{
return (ICommandSink)obj.GetValue(CommandSinkProperty);
}
public static void SetCommandSink(DependencyObject obj, ICommandSink value)
{
obj.SetValue(CommandSinkProperty, value);
}
public static readonly DependencyProperty CommandSinkProperty =
DependencyProperty.RegisterAttached(
"CommandSink",
typeof(ICommandSink),
typeof(CommandSinkBinding),
new UIPropertyMetadata(null, OnCommandSinkChanged));
static void OnCommandSinkChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
ICommandSink commandSink = e.NewValue as ICommandSink;
if (!ConfigureDelayedProcessing(depObj, commandSink))
ProcessCommandSinkChanged(depObj, commandSink);
}
// This method is necessary when the CommandSink attached property
// is set on an element in a template, or any other situation in
// which the element's CommandBindings have not yet had a chance to be
// created and added to its CommandBindings collection.
static bool ConfigureDelayedProcessing(DependencyObject depObj, ICommandSink sink)
{
bool isDelayed = false;
CommonElement elem = new CommonElement(depObj);
if (elem.IsValid && !elem.IsLoaded)
{
RoutedEventHandler handler = null;
handler = delegate
{
elem.Loaded -= handler;
ProcessCommandSinkChanged(depObj, sink);
};
elem.Loaded += handler;
isDelayed = true;
}
return isDelayed;
}
static void ProcessCommandSinkChanged(DependencyObject depObj, ICommandSink sink)
{
CommandBindingCollection cmdBindings = GetCommandBindings(depObj);
if (cmdBindings == null)
throw new ArgumentException("...");
foreach (CommandBinding cmdBinding in cmdBindings)
{
CommandSinkBinding csb = cmdBinding as CommandSinkBinding;
if (csb != null && csb.CommandSink == null)
csb.CommandSink = sink;
}
}
static CommandBindingCollection GetCommandBindings(DependencyObject depObj)
{
var elem = new CommonElement(depObj);
return elem.IsValid ? elem.CommandBindings : null;
}
It is important to note that setting the attached CommandSinkNow that we have seen the fundamental pieces of the solution, let's turn our attention to a very convenient class called Here is the /// <summary>
/// This implementation of ICommandSink can serve as a base
/// class for a ViewModel or as an object embedded in a ViewModel.
/// It provides a means of registering commands and their callback
/// methods, and will invoke those callbacks upon request.
/// </summary>
public class CommandSink : ICommandSink
{
#region Data
readonly Dictionary<ICommand, CommandCallbacks> _commandToCallbacksMap =
new Dictionary<ICommand, CommandCallbacks>();
#endregion // Data
#region Command Registration
public void RegisterCommand(
ICommand command, Predicate<object> canExecute, Action<object> execute)
{
VerifyArgument(command, "command");
VerifyArgument(canExecute, "canExecute");
VerifyArgument(execute, "execute");
_commandToCallbacksMap[command] = new CommandCallbacks(canExecute, execute);
}
public void UnregisterCommand(ICommand command)
{
VerifyArgument(command, "command");
if (_commandToCallbacksMap.ContainsKey(command))
_commandToCallbacksMap.Remove(command);
}
#endregion // Command Registration
#region ICommandSink Members
public virtual bool CanExecuteCommand(
ICommand command, object parameter, out bool handled)
{
VerifyArgument(command, "command");
if (_commandToCallbacksMap.ContainsKey(command))
{
handled = true;
return _commandToCallbacksMap[command].CanExecute(parameter);
}
else
{
return (handled = false);
}
}
public virtual void ExecuteCommand(
ICommand command, object parameter, out bool handled)
{
VerifyArgument(command, "command");
if (_commandToCallbacksMap.ContainsKey(command))
{
handled = true;
_commandToCallbacksMap[command].Execute(parameter);
}
else
{
handled = false;
}
}
#endregion // ICommandSink Members
#region VerifyArgument
static void VerifyArgument(object arg, string argName)
{
if (arg == null)
throw new ArgumentNullException(argName);
}
#endregion // VerifyArgument
#region CommandCallbacks [nested struct]
private struct CommandCallbacks
{
public readonly Predicate<object> CanExecute;
public readonly Action<object> Execute;
public CommandCallbacks(Predicate<object> canExecute, Action<object> execute)
{
this.CanExecute = canExecute;
this.Execute = execute;
}
}
#endregion // CommandCallbacks [nested struct]
}
Using CommandSink in ViewModel ClassesFinally, it is time to see how to use /// <summary>
/// A ViewModel class that exposes a collection of
/// PersonViewModel objects, and provides a routed
/// command that, when executed, kills the people.
/// This class derives from CommandSink, which is
/// why it does not directly implement the ICommandSink
/// interface. See PersonViewModel for an example
/// of implementing ICommandSink directly.
/// </summary>
public class CommunityViewModel : CommandSink
{
public CommunityViewModel()
{
// Populate the community with some people.
Person[] people = Person.GetPeople();
IEnumerable<PersonViewModel> peopleView =
people.Select(p => new PersonViewModel(p));
this.People = new ReadOnlyCollection<PersonViewModel>(peopleView.ToArray());
// Register the command that kills all the people.
base.RegisterCommand(
KillAllMembersCommand,
param => this.CanKillAllMembers,
param => this.KillAllMembers());
}
public ReadOnlyCollection<PersonViewModel> People { get; private set; }
public static readonly RoutedCommand KillAllMembersCommand = new RoutedCommand();
public bool CanKillAllMembers
{
get { return this.People.Any(p => p.CanDie); }
}
public void KillAllMembers()
{
foreach (PersonViewModel personView in this.People)
if (personView.CanDie)
personView.Die();
}
}
The magic occurs in the constructor. Notice the call to the Now let's see how the Below is the constructor of readonly CommandSink _commandSink; public PersonViewModel(Person person) { _person = person; _commandSink = new CommandSink(); _commandSink.RegisterCommand( DieCommand, param => this.CanDie, param => this.Die()); _commandSink.RegisterCommand( SpeakCommand, param => this.CanSpeak, param => this.Speak(param as string)); } I won't bother showing you every member of the class, since they are not very important for this review. We will, however, now see how public bool CanExecuteCommand(ICommand command, object parameter, out bool handled)
{
return _commandSink.CanExecuteCommand(command, parameter, out handled);
}
public void ExecuteCommand(ICommand command, object parameter, out bool handled)
{
_commandSink.ExecuteCommand(command, parameter, out handled);
}
Revision History
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||