I am setting up a Project with WPF and MVVM and was wondering how it could be made easy to hook a Command to an Event.
After searching around on the Internet the only solution I found was with AttachedProperties.
I didnt like the high amount of maintenance with that and tried to find other solutions.
My first attempt was with a MarkupExtension, but obviously the Xaml Parser handles Events differently and doesnt allow for MarkupExtensions as Value.
It was clear that i needed my own layer between the Event and the Command. The only remaining clean place for that is a Setter in a Style or Template.
After 2 days of research I finally came up with a solution that looks like the following in Xaml.
<ad:DockingManager Name="dm1" DocumentsSource="{Binding Pages}">
<ad:DocumentPane />
<ad:DockingManager.Style>
<Style TargetType="ad:DockingManager">
<!-- Routed Event -->
<hlp:EventCommandSetter Event="KeyDown" Command="{StaticResource
RequestCloseCommand}" />
<!-- OldFashioned .Net Event -->
<hlp:EventCommandSetter RegularEvent="RequestDocumentClose" Command="{StaticResource
RequestCloseCommand}" />
</Style>
</ad:DockingManager.Style>
</ad:DockingManager>
I looked at the EventSetter and found that its mostly allready doing what we need. It hooks to a RoutedEvent and calls a Handler.
So if we derive from EventSetter and just set the Handler to our own, that finally calls the Command, everything is fine and working.
But what happens, if we want to hook to an old fashioned .net event? The events in the Avalon DockingManager are not RoutedEvents, for example.
For that we need an encapsulation of the event, like the RoutedEvent class is. I called this class Event and used it in my EventCommandSetter.
public class EventCommandSetter : EventSetter
{
private
ICommand _command;
private
Event _regularEvent;
private static readonly
RoutedEvent _regularRoutedEvent = EventManager.RegisterRoutedEvent(
"RegularRouted",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(EventCommandSetter)
);
private void OnEvent(object
sender, EventArgs e)
{
if (_command == null)
throw new ArgumentException("EventCommandSetter.Command
must not be null!");
EventCommandArgs args = new EventCommandArgs(sender, e);
if (_command.CanExecute(args))
{
_command.Execute(args);
if (typeof(RoutedEventArgs).IsAssignableFrom(e.GetType()))
((RoutedEventArgs)e).Handled = true;
}
}
public
Event RegularEvent
{
get { return
_regularEvent; }
set
{
if (value == null) throw new ArgumentNullException("value");
_regularEvent = value;
this.Event
= _regularRoutedEvent;
}
}
public
ICommand Command
{
get { return _command; }
set
{
if
(value == null)
throw new ArgumentNullException("value");
if
(this.Event == null)
throw new ArgumentException("Either
Event or RegularEvent must be set!");
this.Handler
= Delegate.CreateDelegate(this.Event.HandlerType, this,
((EventHandler)OnEvent).Method);
if
(_regularEvent != null) _regularEvent.Handler
+= new EventHandler(OnEvent);
_command = value;
}
}
}
The Event class is responsible for registering the source Event and passing it to our EventCommandSetter.
Also, I couldn't find any existing TypeConverter for regular Events, so I needed something where I could define a TypeConverter on.
[TypeConverter(typeof(EventConverter))]
public class Event
{
public
Event(object eventSource, EventInfo eventInfo)
{
eventInfo.AddEventHandler(eventSource, Delegate.CreateDelegate(eventInfo.EventHandlerType,
this, ((EventHandler)OnEvent).Method));
}
private void OnEvent(object
sender, EventArgs e)
{
if (this.Handler != null)
this.Handler(sender, e);
}
public event EventHandler
Handler;
}
The class EventCommandArgs just encapsulates the regular Event Parameters into the one that is supported by a Command.
public class EventCommandArgs
{
public
EventCommandArgs(object sender, EventArgs e)
{
this.Sender
= sender;
this.EventArgs
= e;
}
public object Sender { get; set; }
public EventArgs EventArgs { get;
set; }
}
When starting with this, I thought it's easy to write a TypeConverter. It sure is for primitive Types.
First I needed to find the value for the TargetType Property of the Style class. One day of research and 3 lines of code later I finally had it.
Next I needed the instance of the object this TargetType points to. Another day of research and a little bit more lines of code later I even managed this hurdle. I'm not sure if the solution is a solid one, but it passed my primitive tests.
public class EventConverter : TypeConverter
{
private
ServiceType GetService<ServiceType>(ITypeDescriptorContext context)
{
return (ServiceType)context.GetService(typeof(ServiceType));
}
public override bool
CanConvertFrom(ITypeDescriptorContext context, Type
sourceType)
{
if (sourceType == typeof(string)) return true;
return false;
}
public override bool
CanConvertTo(ITypeDescriptorContext context, Type
destinationType)
{
return false;
}
private void AddCurrentChild(object
current, Type targetType, List<object>
coll)
{
if (targetType.IsAssignableFrom(current.GetType()))
coll.Add(current);
if (!typeof(DependencyObject).IsAssignableFrom(current.GetType()))
return;
foreach (object child
in
LogicalTreeHelper.GetChildren((DependencyObject)current))
AddCurrentChild(child, targetType, coll);
}
public
IList FindChildren(IRootObjectProvider root, Type
targetType)
{
List<object>
coll = new List<object>();
AddCurrentChild(root.RootObject,
targetType, coll);
return coll;
}
public override object
ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object
value)
{
if (context == null) return null;
var schema =
GetService<IXamlSchemaContextProvider>(context).SchemaContext;
var ambient =
GetService<IAmbientProvider>(context);
var root =
GetService<IRootObjectProvider>(context);
var targetType = ambient.GetFirstAmbientValue(null,
schema.GetXamlType(typeof(Style)).GetMember("TargetType"),
schema.GetXamlType(typeof(ControlTemplate)).GetMember("TargetType")
);
if (targetType == null)
throw new Exception("Could
not determine TargetType!");
var eventInfo = ((Type)targetType.Value).GetEvent(value.ToString());
if (eventInfo == null)
throw new ArgumentException(value.ToString() + " is no event on " +
targetType.Value.ToString());
var children = FindChildren(root,
eventInfo.DeclaringType);
if (children.Count == 0) throw
new Exception("Could not find instance of " +
eventInfo.DeclaringType.ToString() + "!");
// the last one is the one we currently parse
return new
Event(children[children.Count - 1], eventInfo);
}
public override object
ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object
value, Type destinationType)
{
throw base.GetConvertToException(value,
destinationType);
}
}
If anyone has suggestions to improve this concept, or may has a better solution, please post a comment!