Archive

Posts Tagged ‘MVVM’

Attached Behaviors Part 7: EnumSelector

April 28th, 2011 No 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.)

Attached Behaviors Part 6: EnumGroup

April 26th, 2011 No comments
    Our framework for matching enumeration values has helped us easily manage the Visibility and IsEnabled properties of individual controls. Now, we will write an attached behavior to select a value from a set of radio buttons.

    Scenario

    Radio buttons naturally represent an enumeration: as a related set of controls, known as a group, each is a value in a mutually exclusive selection. For example, a checkout form might offer multiple payment methods, such a credit card or PayPal, and allow the user to choose with radio buttons. Here, we can use enumeration matching to synchronize a property with the selected radio button:

<RadioButton

  Content="Credit card"

  local:EnumGroup.Value="{Binding PaymentType, Mode=TwoWay}"

  local:EnumGroup.TargetValue="CreditCard"

/>

<RadioButton

  Content="PayPal"

  local:EnumGroup.Value="{Binding PaymentType, Mode=TwoWay}"

  local:EnumGroup.TargetValue="PayPal"

/>

Each radio button is associated with an enumeration value. When the user selects an option, it writes that value to the PaymentType property through a two-way binding.

EnumGroup

If you have read the rest of the series, there isn’t much new here. We define the behavior as a static class:

public static class EnumGroup

Next, we register the attached properties which govern the behavior. First is the Value 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 ValueProperty =

  DependencyProperty.RegisterAttached(

    "Value",

    typeof(object),

    typeof(EnumGroup),

    new PropertyMetadata(OnArgumentsChanged));

 

public static object GetValue(RadioButton radioButton)

{

  return radioButton.GetValue(ValueProperty);

}

 

public static void SetValue(RadioButton radioButton, object value)

{

  radioButton.SetValue(ValueProperty, value);

}

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

public static readonly DependencyProperty TargetValueProperty =

  DependencyProperty.RegisterAttached(

    "TargetValue",

    typeof(string),

    typeof(EnumGroup),

    new PropertyMetadata(OnArgumentsChanged));

 

public static string GetTargetValue(RadioButton radioButton)

{

  return (string) radioButton.GetValue(TargetValueProperty);

}

 

public static void SetTargetValue(RadioButton radioButton, string value)

{

  radioButton.SetValue(TargetValueProperty, value);

}

Behavior

The integration of the behavior into the static class is exactly the same as the previous behaviors. 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 EnumGroupBehavior(host));

Then, we update it when either of the Value or TargetValue dependency properties changes:

private static void OnArgumentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

  Behavior.Update(d);

}

The behavior class is more interesting than those for other behaviors because it overrides the Attach and Detach methods to manage a handler for the Checked event:

private sealed class EnumGroupBehavior : Behavior<RadioButton>

{

  private readonly EnumCheck _enumCheck = new EnumCheck();

 

  internal EnumGroupBehavior(DependencyObject host) : base(host)

  {}

 

  protected override void Attach(RadioButton host)

  {

    host.Checked += OnChecked;

  }

 

  protected override void Detach(RadioButton host)

  {

    host.Checked -= OnChecked;

  }

 

  protected override void Update(RadioButton host)

  {

    _enumCheck.Update(GetValue(host), GetTargetValue(host));

 

    host.IsChecked = _enumCheck.IsMatch;

  }

 

  private void OnChecked(object sender, RoutedEventArgs e)

  {

    TryUpdate(host => SetValue(host, _enumCheck.ParsedTargetValue));

  }

}

The Update method, called whenever Value or TargetValue changes (via OnArgumentsChanged), is straightforward. We leverage the EnumCheck class to manage the parsing, caching, and matching. Then, we set the IsChecked property based on whether Value matches TargetValue.

Another interesting aspect of this behavior is the handler for the radio button’s Checked event. This is the first time we have a chance to use the TryUpdate method, which executes the specified lambda expression only if the host has not been garbage-collected.

The Checked event occurs when the radio button becomes the selected value in its group. When that happens, we set the Value property to the associated target value. Voila!

Sample Project

This WPF project shows EnumGroup in action:

Attached Behaviors Part 6 EnumGroup.zip

It allows you to set the value of the EnumGroup.Value attached property and see the results when applied to two radio buttons. You can also check the radio buttons and see EnumGroup.Value change, showing the effect of the two-way binding.

As a side note, the EnumCheck class evolved since the last post. It now performs the duties of EnumTargetValue, which no longer exists. It also has a new member, ParsedTargetValue, which we used in the OnChecked method above. It returns the first value from ParsedTargetValues, which may contain multiple values if TargetValue is set to a comma-separated string.

Summary

We used enumeration matching to put another fundamental building block in place, the radio button group. Their mutually exclusive nature is a seamless fit for selecting one value from many. By associating each radio button with an enumeration value, we easily and intuitively described a choice.

Next time, we will use selectors, such as a combo boxes and list boxes, to select enumeration values.

Attached Behaviors Part 5: EnumIsEnabled

February 23rd, 2011 No comments

Last time, we created a small framework for matching enumeration values. With this very handy technique in our arsenal, we can start interpreting enumeration properties in various ways.

Scenario

A common situation is the need to enable and disable related sets of controls. For example, we might disable credit card fields if a user chooses the PayPal option on a checkout form (and vice versa). In these cases, we can use enumeration matching to manage the UIElement.IsEnabled property:

<StackPanel>

  <local:PayPalOptions

    local:EnumIsEnabled.Value="{Binding PaymentType}"

    local:EnumIsEnabled.TargetValue="PayPal"

  />

  <local:CreditCardOptions

    local:EnumIsEnabled.Value="{Binding PaymentType}"

    local:EnumIsEnabled.TargetValue="CreditCard"

  />

</StackPanel>

Both of the payment options are governed by the PaymentType property. They are mutually exclusive because only one target value will match at a time.

Like with other enumeration-based behaviors, we can invert the results of the match using the WhenMatched and WhenNotMatched properties:

<local:CreditCardOptions

  local:EnumIsEnabled.Value="{Binding PaymentType}"

  local:EnumIsEnabled.TargetValue="CreditCard"

  local:EnumIsEnabled.WhenMatched="False"

  local:EnumIsEnabled.WhenNotMatched="True"

/>

EnumIsEnabled

The naming for this concept is a little fuzzy. My first thought, EnumEnabler, uses a term most commonly associated with addictive behavior. Scratch. EnumEnabled is closer, but it sounds like we are enabling something about enumerations, not setting the IsEnabled property. I ultimately decided the format EnumPropertyName is more discoverable than using an arbitrary term to complete the identifier. Naming suggestions are welcome in the comments.

The process of defining an attached behavior should be very familiar by now. We start by declaring a static class:

public static class EnumIsEnabled

Next, we register the attached properties which govern the behavior. First is the Value 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 ValueProperty =

  DependencyProperty.RegisterAttached(

    "Value",

    typeof(object),

    typeof(EnumIsEnabled),

    new PropertyMetadata(OnArgumentsChanged));

 

public static object GetValue(UIElement uiElement)

{

  return uiElement.GetValue(ValueProperty);

}

 

public static void SetValue(UIElement uiElement, object value)

{

  uiElement.SetValue(ValueProperty, value);

}

Next, we register the TargetValue property, which is a simple string we will parse later:

public static readonly DependencyProperty TargetValueProperty =

  DependencyProperty.RegisterAttached(

    "TargetValue",

    typeof(string),

    typeof(EnumIsEnabled),

    new PropertyMetadata(OnArgumentsChanged));

 

public static string GetTargetValue(UIElement uiElement)

{

  return (string) uiElement.GetValue(TargetValueProperty);

}

 

public static void SetTargetValue(UIElement uiElement, string value)

{

  uiElement.SetValue(TargetValueProperty, value);

}

Then, we register the WhenMatched property, giving it a default value of true:

public static readonly DependencyProperty WhenMatchedProperty =

  DependencyProperty.RegisterAttached(

    "WhenMatched",

    typeof(bool),

    typeof(EnumIsEnabled),

    new FrameworkPropertyMetadata(true, OnArgumentsChanged));

 

public static bool GetWhenMatched(UIElement uiElement)

{

  return (bool) uiElement.GetValue(WhenMatchedProperty);

}

 

public static void SetWhenMatched(UIElement uiElement, bool value)

{

  uiElement.SetValue(WhenMatchedProperty, value);

}

Finally, we register the WhenNotMatched property, giving it a default value of false:

public static readonly DependencyProperty WhenNotMatchedProperty =

  DependencyProperty.RegisterAttached(

    "WhenNotMatched",

    typeof(bool),

    typeof(EnumIsEnabled),

    new FrameworkPropertyMetadata(false, OnArgumentsChanged));

 

public static bool GetWhenNotMatched(UIElement uiElement)

{

  return (bool) uiElement.GetValue(WhenNotMatchedProperty);

}

 

public static void SetWhenNotMatched(UIElement uiElement, bool value)

{

  uiElement.SetValue(WhenNotMatchedProperty, value);

}

Behavior

As in the previous installments in this series, we declare the behavior, telling it how to create instances for individual host objects:

private static readonly AttachedBehavior Behavior =

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

Now, we update it whenever any of the above dependency properties changes:

private static void OnArgumentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

  Behavior.Update(d);

}

The behavior implementation looks almost exactly like the one for EnumVisibility:

private sealed class EnumIsEnabledBehavior : Behavior<UIElement>

{

  private readonly EnumCheck _enumCheck = new EnumCheck();

 

  internal EnumIsEnabledBehavior(DependencyObject host) : base(host)

  {}

 

  protected override void Update(UIElement host)

  {

    _enumCheck.Update(GetValue(host), GetTargetValue(host));

 

    host.IsEnabled = _enumCheck.IsMatch

      ? GetWhenMatched(host)

      : GetWhenNotMatched(host);

  }

}

We once again leverage the EnumCheck class to manage the parsing, caching, and matching of target values.

Sample Project

This WPF application shows EnumIsEnabled in action:

Attached Behaviors Part 5 EnumIsEnabled.zip

It allows you to set the value of the EnumIsEnabled.Value attached property on three different text blocks. The first shows when Value matches TargetValue. The second shows when Value matches a comma-separated TargetValue. The third uses the WhenMatched and WhenNotMatched attached properties to show when Value does not match TargetValue instead.

Summary

Except for the reference to the IsEnabled property, we didn’t really write anything new for this attached behavior. We only have to write code at all because the Value, TargetValue, WhenMatched, and WhenNotMatched properties must be scoped to a specific class in the XAML. This, along with the fact that WhenMatched and WhenNotMatched can have different types, is why we redeclare the properties on each new behavior.

Next time, we will use a group of radio buttons to allow the user to select enumeration values.

Attached Behaviors Part 4: EnumVisibility

February 15th, 2011 No comments

Another convenient behavior is to conditionally show a piece of UI based on the value of an enumeration property. This is especially useful for working with view models in an MVVM context, where enumerations are a common property type.

The input to this behavior is a value and a target value. The host is shown/hidden based on whether they match:

<TextBlock

  Text="Access denied"

  local:EnumVisibility.Value="{Binding UserType}"

  local:EnumVisibility.TargetValue="Standard"

/>

We can specify multiple target values to compare against the Value property:

<TextBlock

  Text="Access granted"

  local:EnumVisibility.Value="{Binding UserType}"

  local:EnumVisibility.TargetValue="Moderator, Administrator"

/>

We can also invert the translation using the WhenMatched and WhenNotMatched properties:

<TextBlock

  Text="Access granted"

  local:EnumVisibility.Value="{Binding UserType}"

  local:EnumVisibility.TargetValue="Standard"

  local:EnumVisibility.WhenMatched="Collapsed"

  local:EnumVisibility.WhenNotMatched="Visible"

/>

An especially nice use is specifying user-friendly names for enumeration values (a traditionally thorny problem). We bind a bank of controls in the same space to the same property, knowing only one will show up at any given time. This gives us maximum flexibility in representing particular enumeration values:

<Grid>

  <TextBlock

    Text="Standard"

    local:EnumVisibility.Value="{Binding UserType}"

    local:EnumVisibility.TargetValue="Standard"

  />

  <TextBlock

    Text="Moderator"

    local:EnumVisibility.Value="{Binding UserType}"

    local:EnumVisibility.TargetValue="Moderator"

    FontWeight="Bold"

  />

  <StackPanel

    local:EnumVisibility.Value="{Binding UserType}"

    local:EnumVisibility.TargetValue="Administrator"

    Orientation="Horizontal">

    <TextBlock Text="Administrator" FontWeight="Bold" />

    <Image Source="admin.jpg" />

  </StackPanel>

</Grid>

This allows us to use bindings, resources, localization, and any other technique to portray user-friendly names.

EnumVisibility

We define the attached behavior as a static class:

public static class EnumVisibility

Next, we register the attached properties which govern the behavior. First is the Value 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 ValueProperty =

  DependencyProperty.RegisterAttached(

    "Value",

    typeof(object),

    typeof(EnumVisibility),

    new PropertyMetadata(OnArgumentsChanged));

 

public static object GetValue(UIElement uiElement)

{

  return uiElement.GetValue(ValueProperty);

}

 

public static void SetValue(UIElement uiElement, object value)

{

  uiElement.SetValue(ValueProperty, value);

}

Next, we register the TargetValue property, which is a simple string we will parse later:

public static readonly DependencyProperty TargetValueProperty =

  DependencyProperty.RegisterAttached(

    "TargetValue",

    typeof(string),

    typeof(EnumVisibility),

    new PropertyMetadata(OnArgumentsChanged));

 

public static string GetTargetValue(UIElement uiElement)

{

  return (string) uiElement.GetValue(TargetValueProperty);

}

 

public static void SetTargetValue(UIElement uiElement, string value)

{

  uiElement.SetValue(TargetValueProperty, value);

}

Then, we register the WhenMatched property, giving it a default value of Visible:

public static readonly DependencyProperty WhenMatchedProperty =

  DependencyProperty.RegisterAttached(

    "WhenMatched",

    typeof(Visibility),

    typeof(EnumVisibility),

    new FrameworkPropertyMetadata(Visibility.Visible, OnArgumentsChanged));

 

public static Visibility GetWhenMatched(UIElement uiElement)

{

  return (Visibility) uiElement.GetValue(WhenMatchedProperty);

}

 

public static void SetWhenMatched(UIElement uiElement, Visibility visibility)

{

  uiElement.SetValue(WhenMatchedProperty, visibility);

}

Finally, we register the WhenNotMatched property, giving it a default value of Collapsed:

public static readonly DependencyProperty WhenNotMatchedProperty =

  DependencyProperty.RegisterAttached(

    "WhenNotMatched",

    typeof(Visibility),

    typeof(EnumVisibility),

    new FrameworkPropertyMetadata(Visibility.Collapsed, OnArgumentsChanged));

 

public static Visibility GetWhenNotMatched(UIElement uiElement)

{

  return (Visibility) uiElement.GetValue(WhenNotMatchedProperty);

}

 

public static void SetWhenNotMatched(UIElement uiElement, Visibility visibility)

{

  uiElement.SetValue(WhenNotMatchedProperty, visibility);

}

Behavior

Just like with BooleanVisibility and NullVisibility, we declare the behavior, telling it how to create instances for individual host objects:

private static readonly AttachedBehavior Behavior =

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

Now, we update it whenever any of the above dependency properties changes:

private static void OnArgumentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

  Behavior.Update(d);

}

The IBehavior implementation is similar to the other attached behaviors:

private sealed class EnumVisibilityBehavior : Behavior<UIElement>

{

  private readonly EnumCheck _enumCheck = new EnumCheck();

 

  internal EnumVisibilityBehavior(DependencyObject host) : base(host)

  {}

 

  protected override void Update(UIElement host)

  {

    _enumCheck.Update(GetValue(host), GetTargetValue(host));

 

    host.Visibility = _enumCheck.IsMatch

      ? GetWhenMatched(host)

      : GetWhenNotMatched(host);

  }

}

The major difference is this behavior uses an object dedicated to checking the value against the target value. This cleans up the code considerably, as the matching logic is not trivial. It also allows us to reuse it in other enumeration-related behaviors.

Enumeration Checks

An enumeration check requires parsing of the target value(s) to the enumeration type. Rather than perform the parse every time, the EnumCheck type caches the parsed target values and only recalculates them when necessary. This stateful nature is why we update the _enumCheck field and then query its IsMatch property when updating the behavior.

The EnumCheck class has three properties:

public object Value { get; private set; }

 

public EnumTargetValue TargetValue { get; private set; }

 

public bool IsMatch { get; private set; }

The interesting thing here is the TargetValue property: the EnumTargetValue class encapsulates the parsing and comparison of target value strings. We initialize it in the constructor to an empty instance:

public EnumCheck()

{

  this.TargetValue = new EnumTargetValue();

}

The Update method, which we call from the behavior’s Update method above, is where we coordinate the match:

public void Update(object value, string targetValue)

{

  var valueChanged = value != this.Value;

  var targetValueChanged = targetValue != this.TargetValue.Text;

 

  if(valueChanged || targetValueChanged)

  {

    if(valueChanged)

    {

      this.Value = value;

    }

 

    this.TargetValue.Update(this.Value, targetValue);

 

    this.IsMatch = this.TargetValue.Matches(this.Value);

  }

}

We determine if either the value or target has changed and, if so, recalculate the target value and the match result. EnumTargetValue performs the core match logic.

Target Values

Using commas, a target value can actually be composed of multiple enumeration values. We represent this in the EnumTargetValue class by exposing a read-only wrapper of a list of parsed values:

private readonly List<object> _parsedValues = new List<object>();

 

public EnumTargetValue()

{

  this.ParsedValues = _parsedValues.AsReadOnly();

}

 

public ReadOnlyCollection<object> ParsedValues { get; private set; }

 

public string Text { get; private set; }

This list is the cache for the enumeration values we parse from the target value string. It is typed as object because we don’t have compile-time knowledge of the type of the bound enumeration – we wait to see the type of the value bound to the EnumVisibility.Value property and use that type to do the parsing.

(Later in this series, we will see why we make the parsed values publicly available.)

We also expose the source text, so we can check to see if it has changed in the beginning of the EnumCheck.Update method above. That method also calls the Update method on EnumTargetValue, which caches the text and repopulates the _parsedValues list:

public void Update(object value, string text)

{

  this.Text = text;

 

  ParseValues(value);

}

In order to parse the text, we need the enumeration value against which it will be compared, which we use to determine the type to pass to the Enum.Parse method. By inferring the enumeration type like this, we avoid requiring developers to specify another attached property alongside Value and TargetValue.

The ParseValues method parses each token in a comma-separated string into an enumeration value of the same type as the bound value:

private void ParseValues(object value)

{

  _parsedValues.Clear();

 

  if(value != null && value is Enum && !String.IsNullOrEmpty(this.Text))

  {

    var parsedValues =

      from targetValue in this.Text.Split(‘,’)

      let trimmedTargetValue = targetValue.Trim()

      where trimmedTargetValue.Length > 0

      select Enum.Parse(value.GetType(), trimmedTargetValue, false);

 

    _parsedValues.AddRange(parsedValues);

  }

}

First, we ensure the bound value is an enumeration and there is target value text to parse. Then, we tokenize the string by commas and perform a query over the resulting token. For each, we trim it, ensure it isn’t empty, and parse it to the enumeration type using a case-sensitive comparison. This gives us a sequence of enumeration values which we then add to the _parsedValues list.

Now that we have the set of parsed values, we have to compare it to the bound value. The EnumCheck.Update method above sets the EnumCheck.IsMatch property by calling the EnumTargetValue.Matches method:

public bool Matches(object value)

{

  return value == null || value.Equals("")

    ? String.IsNullOrEmpty(this.Text)

    : _parsedValues.Contains(value);

}

A null or empty bound value matches a null or empty target value, neatly supporting nullable enumerations without actually having to know the enumeration type. If a value is provided, we determine if it exists in the set of parsed values. The Contains call will ultimately use the GetHashCode and Equals methods on the bound value and each parsed value, shouldering the bulk of the equality-checking work.

Sample Project

This WPF application shows EnumVisibility in action:

Attached Behaviors Part 4 EnumVisibility.zip

It allows you to set the value of the EnumVisibility.Value attached property on three different text blocks. The first shows when Value matches TargetValue. The second shows when Value matches a comma-separated TargetValue. The third uses the WhenMatched and WhenNotMatched attached properties to show when Value does not match TargetValue instead.

Summary

Enumeration matching is useful in many scenarios. The incarnation here, which manages the Visibility property, lays the foundation for enumeration-based behaviors.

Next time, we will use the EnumCheck class to manage the IsEnabled property.