Attached Behaviors Part 1: BooleanVisibility
- 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
The DependencyObject system is at the core of the WPF and Silverlight architectures. It enables many killer features of UI development, including data binding, animation, and an overall declarative style. It is the fruit of years spent developing UI frameworks, addressing the entire idea on a more fundamental level.
One of the more interesting features is attached properties, which allow external entities to store pieces of data within dependency objects. A grid stores an object’s row and column, a canvas stores an object’s coordinates, and a docking container stores dock position. The objects remain blissfully unaware of the orthogonal features.
A particularly useful form of these properties is the attached behavior, an attached property which adds functionality to the object on which it is set. This is similar in effect to extension methods: behaviors can implement anything which uses an object’s public API. They can respond to events, such as TextChanged and Checked/Unchecked, as well as use additional attached properties to refine functionality.
Attached behaviors have been used to implement the command pattern, default buttons, and TreeView selection scrolling, as well as numerous other examples. ASP.NET even has an immensely useful implementation. The flexibility is in the simplicity: a dependency object stores a sub-object which extends it with extra behavior. This subtle form of the Mixin pattern is applicable to many problems often solved by other means.
An Example
A design consequence well-known to WPF and Silverlight developers is the Boolean/Visibility incompatibility. Visibility is an enumeration, which cannot be directly bound to Boolean properties. Instead, the colloquial approach is to define a value converter which translates as necessary:
public sealed class BooleanVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool) value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((Visibility) value) == Visibility.Visible;
}
}
An instance is generally included in an application’s resources:
<Application.Resources>
<ResourceDictionary>
<local:BooleanVisibilityConverter x:Key="booleanVisibilityConverter" />
</ResourceDictionary>
</Application.Resources>
Then, it is used in various bindings:
<TextBlock
Text="An error occurred."
Visibility="{Binding HasError, Converter={StaticResource booleanVisibilityConverter}}"
/>
This is an adequate but heavy way to express the relationship between Visibility and HasError. Imagine that we instead set an attached property dedicated to the conversion:
<TextBlock
Text="An error occurred."
local:BooleanVisibility.Value="{Binding HasError}"
/>
This is a much clearer statement of intent, with no associated application resource. Clean.
Another aspect of the incompatibility is inverting Boolean values. I have used three approaches in the past:
- Add ConverterParameter=’Not’ to the binding
- Create a BooleanNotVisibilityConverter
- Add WhenTrue and WhenFalse properties to BooleanVisibilityConverter and create multiple resources
- Each of these also feels heavy. The approach with attached properties has less friction:
<TextBlock
Text="An error occurred."
local:BooleanVisibility.Value="{Binding HasError}"
local:BooleanVisibility.WhenTrue="Collapsed"
local:BooleanVisibility.WhenFalse="Visible"
/>
This neatly addresses WPF’s three-state Visibility enumeration by forcing a choice between Collapsed and Hidden. The Silverlight version works the same way, sans the Hidden option.
BooleanVisibility
Since the API is defined solely in terms of attached properties, we define it as a static class (a nice encapsulation of the feature):
public static class BooleanVisibility
First, we register the Value property, giving it a default value of true to match the UIElement.Visibility property’s default value of Visible. We also create static accessors to facilitate XAML usage:
public static readonly DependencyProperty ValueProperty =
DependencyProperty.RegisterAttached(
"Value",
typeof(bool),
typeof(BooleanVisibility),
new PropertyMetadata(true, OnArgumentChanged));
public static bool GetValue(UIElement uiElement)
{
return (bool) uiElement.GetValue(ValueProperty);
}
public static void SetValue(UIElement uiElement, bool value)
{
uiElement.SetValue(ValueProperty, value);
}
Next, we register the WhenTrue property, giving it a default value of Visible to match Value‘s default:
public static readonly DependencyProperty WhenTrueProperty =
DependencyProperty.RegisterAttached(
"WhenTrue",
typeof(Visibility),
typeof(BooleanVisibility),
new PropertyMetadata(Visibility.Visible, OnArgumentChanged));
public static Visibility GetWhenTrue(UIElement uiElement)
{
return (Visibility) uiElement.GetValue(WhenTrueProperty);
}
public static void SetWhenTrue(UIElement uiElement, Visibility visibility)
{
uiElement.SetValue(WhenTrueProperty, visibility);
}
Finally, we register the WhenFalse property, giving it a default value of Collapsed as the opposite of Value‘s default:
public static readonly DependencyProperty WhenFalseProperty =
DependencyProperty.RegisterAttached(
"WhenFalse",
typeof(Visibility),
typeof(BooleanVisibility),
new PropertyMetadata(Visibility.Collapsed, OnArgumentChanged));
public static Visibility GetWhenFalse(UIElement uiElement)
{
return (Visibility) uiElement.GetValue(WhenFalseProperty);
}
public static void SetWhenFalse(UIElement uiElement, Visibility visibility)
{
uiElement.SetValue(WhenFalseProperty, visibility);
}
Attaching the Behavior
We create one additional attached property to hold the behavior associated with each object:
private static readonly DependencyProperty BehaviorProperty =
DependencyProperty.RegisterAttached(
"Behavior",
typeof(BooleanVisibilityBehavior),
typeof(BooleanVisibility));
The fact that we use a dependency property to associate behavior with objects is an implementation detail, so we can make it private and omit the get/set methods. BooleanVisibilityBehavior, discussed later, is the class which implements the specification.
To attach the behavior, we simply set this property. A change in Value, WhenTrue, or WhenFalse is the cue to attach it if necessary and update its state. Each property registers the OnArgumentChanged method to be called when its value changes:
private static void OnArgumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uiElement = d as UIElement;
if(uiElement != null)
{
var behavior = uiElement.GetValue(BehaviorProperty) as BooleanVisibilityBehavior;
if(behavior == null)
{
behavior = new BooleanVisibilityBehavior(uiElement);
uiElement.SetValue(BehaviorProperty, behavior);
}
behavior.Update();
}
}
First, we ensure an instance of the behavior has been attached to the UI element through BehaviorProperty. Then, we update its state, which sets the Visibility property based on the values of Value, WhenTrue, and WhenFalse.
BooleanVisibilityBehavior
The behavior implements roughly the same functionality as BooleanVisibilityConverter:
- Update the UI element’s Visibility property when Value, WhenTrue, or WhenFalse changes
- Update Value when the UI element’s Visibility property changes
A key difference is that the behavior must listen for changes in the Visibility property, while the converter is not responsible for coordination of any kind.
The behavior class is an implementation detail as well, so we can also make it private. This is convenient because the Get/Set methods for Value, WhenTrue, and WhenFalse are in scope:
private sealed class BooleanVisibilityBehavior
{
private readonly WeakReference _uiElementReference;
internal BooleanVisibilityBehavior(UIElement uiElement)
{
_uiElementReference = new WeakReference(uiElement);
var visibilityDescriptor = DependencyPropertyDescriptor.FromProperty(
UIElement.VisibilityProperty,
uiElement.GetType());
visibilityDescriptor.AddValueChanged(uiElement, OnVisibilityChanged);
}
internal void Update()
{
var uiElement = (UIElement) _uiElementReference.Target;
if(uiElement != null)
{
uiElement.Visibility = GetValue(uiElement)
? GetWhenTrue(uiElement)
: GetWhenFalse(uiElement);
}
}
private void OnVisibilityChanged(object sender, EventArgs e)
{
var uiElement = (UIElement) _uiElementReference.Target;
if(uiElement != null)
{
var value = uiElement.Visibility == GetWhenTrue(uiElement);
SetValue(uiElement, value);
}
}
}
We store the UI element in a WeakReference, which ensures we don’t accidentally keep it alive after it goes out of scope. Then, we use the dependency property system to add a handler for changes in the object’s Visibility property.
The Update method, which we call from the OnArgumentsChanged handler, sets the Visibility property based on the values of the argument properties. We must first ensure the UI element has not been garbage-collected.
The OnVisibilityChanged handler is the other binding direction, setting the Value property based on the value of the Visibility property.
Generalizing Attached Behaviors
The above approach is solid, but wordy. There are a lot of aspects to consider and details to get right. Writing a new attached behavior is a high-friction undertaking which can be intimidating enough to deter developers even when the effort is warranted. This concept is screaming for a framework.
Following is a proposed syntax for defining attached behaviors. It encapsulates some of the stickier points, such as the attachment logic and the weak reference. Part 2 of this series will contain the framework implementation.
First, instead of registering an attached property to hold the behavior, we raise the level of abstraction and register an attached behavior:
private static readonly AttachedBehavior Behavior =
AttachedBehavior.Register(host => new BooleanVisibilityBehavior(host));
We tell the AttachedBehavior class how to create instances of the behavior for objects which will host it. Next, we update the host’s behavior when any of its arguments changes:
private static void OnArgumentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Behavior.Update(d);
}
Finally, we define the behavior:
private sealed class BooleanVisibilityBehavior : Behavior<UIElement>
{
internal BooleanVisibilityBehavior(DependencyObject host) : base(host)
{
var visibilityDescriptor = DependencyPropertyDescriptor.FromProperty(
UIElement.VisibilityProperty,
host.GetType());
visibilityDescriptor.AddValueChanged(host, OnVisibilityChanged);
}
protected override void Update(UIElement host)
{
host.Visibility = GetValue(host) ? GetWhenTrue(host) : GetWhenFalse(host);
}
private void OnVisibilityChanged(object sender, EventArgs e)
{
TryUpdate(host =>
{
var value = host.Visibility == GetWhenTrue(host);
SetValue(host, value);
});
}
}
We tell the base class the expected type of the host and pass the instance through to its constructor; it is responsible for the weak reference. We override Update and perform the logic without worrying about any null-checking, as the base class will ensure the host has not been garbage-collected.
OnVisibilityChanged, though, also needs access to the host. To do so, we use the TryUpdate method, giving it a lambda expression which it will only execute if the host has not been garbage-collected.
That’s it! This is much more straightforward and declarative than the raw approach. The majority of the code is concerned with the behavior itself and not with managing the instance. This style is more accessible in our everyday work, where attached behaviors may simply replace some converters.
Summary
Attached behaviors are a powerful tool in the toolkit of any WPF or Silverlight developer. They neatly encapsulate the relationship between an object and the values which drive its behavior. Their implementation complexity, though, is a large deterrent to their widespread adoption. By simplifying or removing many of the details, this framework lowers the barrier to entry and clears the path for attached behaviors to become a more common technique.
Next time, we will see how to implement the AttachedBehavior and Behavior<> classes.