Home > Uncategorized > Attached Behaviors Part 4: EnumVisibility

Attached Behaviors Part 4: EnumVisibility

February 15th, 2011 Leave a comment Go to 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.