Thursday, October 30, 2008

RoutedEvents to Commands

Microsoft started down a great road by introducing Commanding with WPF. However, in my opinion they didn't go far enough. While it's great that some UI elements like Buttons, Menus, etc. support sending a Command when activated, there are many other places in UI design that an action taken implies a command. The most common example I can think of is when the SelectedItem value changes in a Listbox or Combobox. Perhaps you want to either display details about a selected listbox item. Maybe you want to take action if the user double-clicks the list item. Maybe you want to immediately do some action, like a user chooses a value from a ComboBox and your ready to move forward. (I won't go into whether this is really good UI design).

Regardless, my point is, there are lots of things exposed via RoutedEvents that have no command interface, but it sure would be nice to have a command instead of a routed command.

One great thing about WPF is the extensibility. Using the Attached Behavior pattern (by John Gossman) we can create a class that will allow us to add command patterns onto UIElement for any routed command. The code is at the end of this post.

Some interesting things I learned while making this class and then trying to use it. One, XAML is good, but it's got a long way to go to catch up to the more mature .NET languages, like C#. A difficulty in XAML is the lack of generic support, so you need to do little things like my creation of the RoutedEventCommandBindingCollection class, even though it is an empty class, it creates a concrete class of the generic ObservableCollection<>.

The fact that attached property instances are not separately initialized also was a problem. For example, it would be great to initialize each attached property to be an empty list. However, we can't do this. So instead, we need to do the initialization in the XAML, as seen in the code example below. If you exclude the RoutedEventCommandBindingCollection wrapping, you get a NULL exception because the attached property EventCommandBinding (which is a list) hasn't been initialized to an empty list.


Using the class in XAML:



<UWPath:RoutedEventCommandProxy.EventCommandBinding>
<UWPath:RoutedEventCommandBindingCollection>
<UWPath:RoutedEventCommandBinding Event="UIElement.MouseUp" Command="{StaticResource MouseWasClicked}"/>
</UWPath:RoutedEventCommandBindingCollection>
</UWPath:RoutedEventCommandProxy.EventCommandBinding>



The source code in C#:



public class RoutedEventCommandProxy
{
// Fun, this will keep track of all the bindings!
private static Dictionary<UIElement, RoutedEventCommandBindingCollection> handlerTable =
new Dictionary<UIElement, RoutedEventCommandBindingCollection>();

public static readonly DependencyProperty EventCommandBindingProperty =
DependencyProperty.RegisterAttached("EventCommandBinding",
typeof(RoutedEventCommandBindingCollection),
typeof(RoutedEventCommandProxy),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(PropertyChanged)));

public static void SetEventCommandBinding(UIElement element, RoutedEventCommandBindingCollection value)
{
element.SetValue(EventCommandBindingProperty, value);
}
public static RoutedEventCommandBindingCollection GetEventCommandBinding(UIElement element)
{
return (RoutedEventCommandBindingCollection)element.GetValue(EventCommandBindingProperty);
}

private static void PropertyChanged (DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
UIElement element = sender as UIElement;

// Remove any old stuff..
if (handlerTable.ContainsKey(element))
handlerTable.Remove(element);

RoutedEventCommandBindingCollection OldMappings = (RoutedEventCommandBindingCollection)args.OldValue;
if (OldMappings != null)
{
foreach (RoutedEventCommandBinding mapping in OldMappings)
{
if (mapping.Event != null)
element.RemoveHandler(mapping.Event,new RoutedEventHandler(Handler));
}
}

// Add the new stuff
RoutedEventCommandBindingCollection NewMappings = (RoutedEventCommandBindingCollection)args.NewValue;
if (NewMappings != null)
{
handlerTable.Add(element, NewMappings);
foreach (RoutedEventCommandBinding mapping in NewMappings)
{
if (mapping.Event != null && mapping.Command != null)
element.AddHandler(mapping.Event, new RoutedEventHandler(Handler));
}
}

}

private static void Handler (object sender, RoutedEventArgs e)
{
UIElement element = sender as UIElement;
if (handlerTable.ContainsKey(element))
{
RoutedEventCommandBindingCollection mappings = handlerTable[element];
foreach (RoutedEventCommandBinding mapping in mappings)
{
if (e.RoutedEvent == mapping.Event)
{
mapping.Command.Execute(e);
}
}
}
}
}

public class RoutedEventCommandBindingCollection : ObservableCollection<RoutedEventCommandBinding>
{
}

public class RoutedEventCommandBinding
{
public RoutedEvent Event { get; set; }
public ICommand Command { get; set; }
}

No comments:

Post a Comment