Introduction
In my previous article (
MVVM Design Pattern) I covered the MVVM infrastructure that was lacking commanding. This one completes the previous article by adding commands to the Home Grown MVVM.
How Commanding Works Here
I am pretty sure that Silverlight 5 will have much better support for commands but right now I had to come up with some scheme to support commanding.
The way I 'wired' commands is by 'kidnapping' the events and rewire them so the command action will get invoked.
IMHO some events should remain in the code behind because they are more of a UI functionality than business or data depended functionality. But if I want to be able to do everything via commanding it requires some manipulation and more than just manipulation - certain event information I do not know how to get via XAML binding. Certain actions may require a reference to the control. Other action may need an unrelated data. Based on the above I want my command to deliver 3 objects - the sender, the original event data, and a user (XAML bound) parameter.
I also wanted to be able to bind and specify the Command with a very simple XAML line. No resources etc.
The following is not rocket science, but is not simple if you haven't done it before. If I lose you, go through the code section it may clarify it. In order to not to drawn in details the following description skips and hides few points - I'll cover them later.
So here is how it works: via the XAML I bind a property (attached dependency property) to an instance of CommandBase class. CommandBase is a class that keeps references to the command's action and other related data. When the view is rendered the binding to this property causes an execution of a callback that is specified in the dependency property 'registration'. The attached property is static but the callback delivers instances. It delivers an instance of the control and an instance of the command I bind to in the XAML. Next is the kidnaping/rewiring - I take the original event and register to it a local generic handler. At that point the binding activity is done. As a result to the above what I have is a generic handler that will run when the event is fired.
This handler receives the original events parameters, and the event sender. It also has the ability to pull the command object (I'll cover it later) and to pull the parameter the user might have specified (I'll cover it later). It combines the 3 objects into one ParameterWrapper object. Using the command object it calls the command's action and passes it the ParameterWrapper object.
CommandBase Class
This class implements the ICommand interface which defines the action and check to see if the action can be invoked at this moment. Silverlight 4 uses it with the existing built in commands. It contains 2 delegates - one that is called to see if the action can be run and one which is the action itself.
public class CommandBase
: ICommandFromEvent
{
private bool
canExecuteLocal = false;
private readonly Func<object, bool> isExecuteAllowed = null;
private readonly Action<object>
commandAction = null;
public CommandBase(Action<object> commandDelegate, Func<object, bool>
canExecuteDelegate = null)
{
commandAction
= commandDelegate;
isExecuteAllowed
= canExecuteDelegate;
}
It also implements a custom interface that allows me to call just the one method and the isAllowed check and execution will be done.
CommandBinder Class
This class binds a xaml declared CommandBase to a control and an event. Every event type requires its own CommandBiner.
The commandBinder contains the name of the event that it will 'kidnap'. It also contains 2 (static) attached properties: Command and ComandParameter. Each of these properties requires their respective (static) getter and setter. The Command property also requires the (static) callback. The callback instantiate a class I call Executor. The Executor class is the one that kidnaps the event (rewire it as well as calling the action from the CommandBase).
Here is the first part of the code:
private const string
EName = "SelectionChanged"; //the
event name
public static readonly
DependencyProperty CommandProperty = DependencyProperty.RegisterAttached
(
"Command", //Property name
typeof(ICommand), //Property type
typeof(SelectionChangedCommandBinder), //Type of property owner
new PropertyMetadata(CommandChanged) //Callback invoked on property value has
change
);
public static void SetCommand(DependencyObject
depObj, ICommand cmd)
{
depObj.SetValue(CommandProperty,
cmd);
}
public static ICommand GetCommand(DependencyObject
depObj)
{
return (ICommand)depObj.GetValue(CommandProperty);
}
//the callback
protected static void CommandChanged(DependencyObject
depObj, DependencyPropertyChangedEventArgs
depArgs)
{//create an Executor for a given even and event-args type
var executor = new Executor<RoutedPropertyChangedEventArgs<object>>(EName);
executor.CommandGetter
= GetCommand;
executor.ParameterGetter
= GetCommandParameter;
executor.CommandChanged(depObj,
depArgs);
}
A similar code exists for the CommandParameterProperty (minus the callback).
The CommandChanged (callback) instantiates a new Executor for every new binding. This is the place in which the static nature of the attached property 'stops' and a new instance is being assign to each and every event/control/command. While instantiating an Executor class the callback passes to it the name of the event, a delegate to find the BaseCommand and a delegate to find the CommandParameter. Since the Executor is a Generic class the callback implicitly specify the type of the event arg type.
The Executor Class
This generic class wires a generic event handler to the event. Firstly, by using reflection it finds the event based on its name, next it create a delegate out of the local generic handler, lastly it adds this delegate to the event so that next time the event is raised it will invoke the local handler.
public void CommandChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs
depArgs)
{ //use reflection to add a
given handler as a delegate to the given event name
if (depObj != null)
{
Type type =
depObj.GetType();
var eventInfo =
type.GetEvent(eventName);
if (eventInfo != null)
{ //event name yield an event
var delegateType =
eventInfo.EventHandlerType;
if (delegateType != null)
{ //create a delegate from the handler
Delegate handler = Delegate.CreateDelegate(delegateType,
this, "TheHandler");
if (handler != null)
{
// delegate is good, add it to the event.
eventInfo.AddEventHandler(depObj, handler);
}
}
}
else
{
MessageBox.Show(string.Format("missing types for Event [{0}] in [{1}]",
eventName, depObj));
}
}
}
The local generic handler uses the delegates that were passed in and calls the command's action.
private void TheHandler(object sender, T e)
{
var commander = sender as
DependencyObject;
if (commander != null)
{ //get the CommandBase
var cmd =
commandGetter(commander);
if (cmd != null)
{
//wrap all the data by one object
var param =
new ParameterWrapper(parameterGetter(commander),
sender, e as RoutedEventArgs);
var sCmd =
cmd as ICommandFromEvent;
if (sCmd != null)
{
//as part of this call check if the command is
allowed to be executed
sCmd.SmartExecute(param);
}
else
{
//Execute checking might have been done earlier
cmd.Execute(param);
}
}
}
}
XAML Declaration of the Command
The nice part of all of this commanding design is the ease with which I declare the command and the command parameter in XAML:
<sdk:DataGrid AutoGenerateColumns="True"
HorizontalAlignment="Left"
Margin="5"
VerticalAlignment="Top"
Grid.Row="1"
ItemsSource="{Binding DataToPresent}"
cmd:SelectionChangedCommandBinder.Command="{Binding ItemSelectedCommand}"
cmd:SelectionChangedCommandBinder.CommandParameter=
"{Binding SelectedItem, RelativeSource={RelativeSource self}}"
/>
The Above XAML snippet declares the DataGrid the last 2 lines deal with the command declaration.
First I bind the ItemSelectCommand (represent an instance of CommandBase) with the Command Attached Property that resides in the SelectionChangedCommandBinder class.
The second line uses some XAML acrobatics to bind the SelectedItem property of the current control to the CommandParameter Attached Property that resides in the SelectionChangedCommandBinder class.
Steps to Create a Command
Every event that I want to convert to a command requires its own class with the 2 attached properties. The good news is that most apps need a limited number of event types. Also the creation process is quite simple, but process nonetheless.
- Create new command binder (I follow a name convention therefore I copy an existing one and then replace old event name by new one [I clear search criteria 'Match whole Word' - I get 6 replacements).
- In the callback of command section instantiate the Executor with the correct event handler type
- Create a new command property (I always do it in the BaseViewModel in the region 'command properties')
- Add a new Action (I create a virtual one in the BaseViewModel in the region 'command actions'
- Instantiate the CommandBase in BaseViewModel in the Initialize method
- In the specific view bind the command in the XAML.
Step No. 1
public class KeyDownCommandBinder
{
private const string
EName = "KeyDown";
public static readonly
DependencyProperty CommandParameterProperty
= DependencyProperty.RegisterAttached
(
"CommandParameter", //Property
name
typeof(object), //Property
type
typeof(KeyDownCommandBinder), //Type of property owner
new PropertyMetadata(CommandParameterChanged) //Callback invoked
on property value has change
);
public static void
SetCommandParameter(DependencyObject depObj,
object param)
{
depObj.SetValue(CommandParameterProperty,
param);
}
public static object
GetCommandParameter(DependencyObject depObj)
{
return depObj.GetValue(CommandParameterProperty);
}
private static void
CommandParameterChanged(DependencyObject
depObj, DependencyPropertyChangedEventArgs
depArgs)
{
}
public static readonly
DependencyProperty CommandProperty = DependencyProperty.RegisterAttached
(
"Command", //Property name
typeof(ICommand), //Property type
typeof(KeyDownCommandBinder), //Type of
property owner
new
PropertyMetadata(CommandChanged) //Callback
invoked on property value has change
);
public static void
SetCommand(DependencyObject depObj, ICommand cmd)
{
depObj.SetValue(CommandProperty,
cmd);
}
public static ICommand
GetCommand(DependencyObject depObj)
{
return (ICommand)depObj.GetValue(CommandProperty);
}
public static void
CommandChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs depArgs)
{
var executor = new Executor<KeyEventArgs>(EName);
executor.CommandGetter
= GetCommand;
executor.ParameterGetter
= GetCommandParameter;
executor.CommandChanged(depObj,
depArgs);
}
}
Step No. 2
private ICommand keyDownCommand = null;
public ICommand KeyDownCommand
{
get { return
keyDownCommand; }
set
{
keyDownCommand = value;
OnPropertyChanged("KeyDownCommand");
}
}
Step No. 3
protected virtual void KeyDownAction(object
param)
{
MessageBox.Show("KeyDown
Action is not ready");
}
Step No. 4
KeyDownCommand = new CommandBase(KeyDownAction);
Step No. 5
cmd:KeyDownCommandBinder.Command="{Binding KeyDownCommand}"
cmd:KeyDownCommandBinder.CommandParameter="{Binding Text, RelativeSource={RelativeSource
self}}"
or
cmd:LoadedCommandBinder.Command="{Binding LoadedCommand}"
cmd:LoadedCommandBinder.CommandParameter="[ClassicView]"