Home > Uncategorized > Attached Behaviors Part 7: EnumSelector

Attached Behaviors Part 7: EnumSelector

April 28th, 2011 Leave a comment Go to comments

Last time, we explored the concept of a user making a choice between various enumeration values. We implemented the choice as a set of radio buttons, where each represents a different value and all are bound to the same property. Radio buttons are one of many controls which allow the user to select from various choices. There is a family of controls, such as ComboBox and ListBox, which inherit their selection capabilities from a common primitive, the Selector control. In this installment, we will write an attached behavior to select an enumeration value from any implementation of Selector.

Scenario

Selectors have two important elements: a collection of child objects, and a currently selected object out of that collection. This mimics a choice between enumeration values quite nicely. For example, a checkout form might offer multiple payment methods, such a credit card or PayPal, and allow the user to choose with a combo box. Here, we can use enumeration matching to synchronize a property with the selected item:

<ComboBox local:EnumSelector.SelectedValue="{Binding PaymentType, Mode=TwoWay}">

  <ComboBoxItem Content="Credit card" local:EnumSelector.ItemValue="CreditCard" />

  <ComboBoxItem Content="PayPal" local:EnumSelector.ItemValue="PayPal" />

</ComboBox>

The combo box’s selected value is bound to the PaymentType property and each item is associated with an enumeration value. When the user changes the selection, the attached behavior writes the selected value to the PaymentType property through a two-way binding.

NOTE: The current incarnation of this behavior requires a ComboBoxItem for each value. This is because the items in the selector must have the ItemValue attached property set directly on them. If we place, say, a TextBox directly within the ComboBox, and set the ItemValue property on it, the ComboBox will generate a ComboBoxItem for us, but it won’t have the ItemValue property set. We might be able to check the Content property of each ComboBoxItem for ItemValue, but that is for a future iteration.

EnumSelector

Once again, we define the behavior as a static class:

public static class EnumSelector

Next, we register the attached properties which govern the behavior. First is the SelectedValue property, which can contain any enumeration value and thus needs to be typed as object. We also create static accessors to facilitate XAML usage:

public static readonly DependencyProperty SelectedValueProperty =

  DependencyProperty.RegisterAttached(

    "SelectedValue",

    typeof(object),

    typeof(EnumSelector),

    new PropertyMetadata(OnSelectedValueChanged));

 

public static object GetSelectedValue(Selector selector)

{

  return selector.GetValue(SelectedValueProperty);

}

 

public static void SetSelectedValue(Selector selector, object value)

{

  selector.SetValue(SelectedValueProperty, value);

}

Then, we register the ItemValue property, which is a simple string we will parse later:

public static readonly DependencyProperty ItemValueProperty =

  DependencyProperty.RegisterAttached(

    "ItemValue",

    typeof(string),

    typeof(EnumSelector),

    new PropertyMetadata(OnItemValueChanged));

 

public static string GetItemValue(DependencyObject item)

{

  return (string) item.GetValue(ItemValueProperty);

}

 

public static void SetItemValue(DependencyObject item, string value)

{

  item.SetValue(ItemValueProperty, value);

}

Behavior

The nice part about the attached behavior framework is that most of the code which goes into behaviors is fairly boilerplate. First, we declare the behavior and tell it how to create instances for individual host objects:

private static readonly AttachedBehavior Behavior =

  AttachedBehavior.Register(host => new EnumSelectorBehavior(host));

Then, we update it when the SelectedValue dependency property changes:

private static void OnSelectedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

  Behavior.Update(d);

}

This attached behavior has an extra step, though: we need to update the behavior not only when SelectedValue changes on the selector, but also when ItemValue changes on any item:

private static void OnItemValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

  var item = d as FrameworkElement;

 

  if(item != null)

  {

    var selector = item.Parent as Selector;

 

    if(selector != null)

    {

      Behavior.Update(selector);

    }

  }

}

The behavior implementation is also more involved. The previous enumeration matching behaviors each had one match to perform; this behavior, however, has one match per item. This adds a some complexity but not much.

The class declaration is similar to other behaviors:

private sealed class EnumSelectorBehavior : Behavior<Selector>

As is the constructor:

internal EnumSelectorBehavior(DependencyObject host) : base(host)

{}

The first major difference is that we store an index associating items with their enumeration checks:

private readonly IDictionary<object, EnumCheck> _itemEnumChecks =

  new Dictionary<object, EnumCheck>();

The Attach and Detach overrides manage a handler for the selector’s SelectionChanged event:

protected override void Attach(Selector host)

{

  host.SelectionChanged += OnSelectionChanged;

}

 

protected override void Detach(Selector host)

{

  host.SelectionChanged -= OnSelectionChanged;

 

  _itemEnumChecks.Clear();

}

In the Detach method, we make sure to remove all references to items and their associated enumeration checks.

The Update method goes through all of the items, updates the enumeration check associated with each one, and sets the selector’s SelectedIndex property to the first one which matches:

protected override void Update(Selector host)

{

  var selectedValue = GetSelectedValue(host);

 

  for(var index = 0; index < host.Items.Count; index++)

  {

    var item = host.Items[index];

 

    var itemEnumCheck = GetItemEnumCheck(item);

 

    itemEnumCheck.Update(selectedValue, GetItemValue(item));

 

    if(itemEnumCheck.IsMatch)

    {

      host.SelectedIndex = index;

 

      break;

    }

  }

}

The GetItemEnumCheck method gets the EnumCheck associated with the specified item. If one doesn’t yet exist, we create and cache it:

private EnumCheck GetItemEnumCheck(object item)

{

  EnumCheck itemEnumCheck;

 

  if(!_itemEnumChecks.TryGetValue(item, out itemEnumCheck))

  {

    itemEnumCheck = new EnumCheck();

 

    _itemEnumChecks[item] = itemEnumCheck;

  }

 

  return itemEnumCheck;

}

The GetItemValue method is a bit of glue which attempts to convert an item to a DependencyObject and, if successful, accesses its ItemValue attached property:

private static string GetItemValue(object item)

{

  var dependencyObjectItem = item as DependencyObject;

 

  return dependencyObjectItem == null

    ? null

    : EnumSelector.GetItemValue(dependencyObjectItem);

}

These methods implement the updating of the selector based on the SelectedValue and ItemValue attached properties.

The other side of the coin, updating the SelectedValue attached property when the selector changes, is implemented by the OnSelectionChanged event handler. We again use the TryUpdate method, passing it the name of another method, UpdateSelectedValue, which will only be invoked if the host object hasn’t been garbage-collected:

private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)

{

  TryUpdate(UpdateSelectedValue);

}

The UpdateSelectedValue method updates the enumeration check associated with the selected item and updates the SelectedValue attached property of the host with the selected item’s value:

private void UpdateSelectedValue(Selector host)

{

  var selectedValue = GetSelectedValue(host);

 

  var itemEnumCheck = GetItemEnumCheck(host.SelectedItem);

 

  itemEnumCheck.Update(selectedValue, GetItemValue(host.SelectedItem));

 

  var parsedTargetValue = itemEnumCheck.ParsedTargetValue;

 

  if(selectedValue != null && !selectedValue.Equals(parsedTargetValue))

  {

    SetSelectedValue(host, parsedTargetValue);

  }

}

The if statement prevents an infinite loop where an update to the SelectedValue property raises the SelectionChanged event which updates the SelectedValue property which raises the SelectionChanged event, etc.

Sample Project

This WPF project shows EnumSelector in action:

Attached Behaviors Part 7 EnumSelector.zip

It allows you to set the value of the EnumSelector.SelectedValue attached property and see the results when applied to a ListBox. You can also selected different items in the ListBox and see EnumSelector.SelectedValue change, showing the effect of the two-way binding.

Summary

We used enumeration matching to allow the user to select an enumeration value from many. Like radio buttons, selectors naturally represent a choice between mutually exclusive values. The behavior can be applied to anything which implements Selector, offering a large amount of flexibility and extensibility.

What Next?

These are all the attached behaviors I have for now. They cover the various situations I have encountered in my XAML-related career, mostly in an MVVM context. I am sure I will discover more, though, so I will post write-ups when I do.

My personal utilities library, Cloak, contains the framework as well as all the behaviors featured in this series, for both WPF and Silverlight. (There is a lot of other goodness in there as well.) Download it and give them a try!

(If you found this series useful or maybe even enjoyed it, leave a comment. It’s always nice to be reminded I’m not writing in a void.)