Attached Behaviors Part 4: EnumVisibility
- Attached Behaviors Part 1: BooleanVisibility
- Attached Behaviors Part 2: Framework
- Attached Behaviors Part 3: NullVisibility
- Attached Behaviors Part 4: EnumVisibility
- Attached Behaviors Part 5: EnumIsEnabled
- Attached Behaviors Part 6: EnumGroup
- Attached Behaviors Part 7: EnumSelector
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:
We can specify multiple target values to compare against the Value property:
We can also invert the translation using the WhenMatched and WhenNotMatched properties:
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:
This allows us to use bindings, resources, localization, and any other technique to portray user-friendly names.
We define the attached behavior as a static class:
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:
Next, we register the TargetValue property, which is a simple string we will parse later:
Then, we register the WhenMatched property, giving it a default value of Visible:
Finally, we register the WhenNotMatched property, giving it a default value of Collapsed:
Just like with BooleanVisibility and NullVisibility, we declare the behavior, telling it how to create instances for individual host objects:
Now, we update it whenever any of the above dependency properties changes:
The IBehavior implementation is similar to the other attached behaviors:
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.
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:
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:
The Update method, which we call from the behavior’s Update method above, is where we coordinate the match:
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.
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:
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:
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:
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:
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.
This WPF application shows EnumVisibility in action:
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.
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.