Attached Behaviors Part 2: Framework
- 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
Last time, we devised a simpler syntax for defining attached behaviors in WPF and Silverlight. The new system approaches the concept at a higher level, encapsulating many of the rote mechanics common to the various implementations found on the web. It aims to make attached behaviors an everyday option, using an approach similar to many core WPF systems.
Now, we will flesh out the framework behind the API, which models three distinct aspects of attached behaviors:
- Existence: Declare an object representing the attached behavior
- Lifecycle: Attach and detach behavior instances to/from host objects
- Logic: Update host objects based on changes in bound values
See the end of this post for a solution containing the framework and a usage sample.
Existence
- Attached behaviors, similar to dependency properties, are represented by static fields:
private static readonly AttachedBehavior Behavior =
AttachedBehavior.Register(host => new BooleanVisibilityBehavior(host));
The registration of the behavior of the BooleanVisibility class from part 1
The Register method creates an instance of AttachedBehavior, which encapsulates the ability to attach the behavior to host objects. The lambda expression indicates how to create instances of the behavior when required.
Here is the implementation:
public static AttachedBehavior Register(Func<DependencyObject, IBehavior> behaviorFactory)
{
return new AttachedBehavior(RegisterProperty(), behaviorFactory);
}
Each attached behavior needs its own dependency property to store the IBehavior instance on each host. Rather than require behavior writes to register properties, we (carefully) create them programmatically:
private static DependencyProperty RegisterProperty()
{
return DependencyProperty.RegisterAttached(
GetPropertyName(),
typeof(IBehavior),
typeof(AttachedBehavior));
}
private static string GetPropertyName()
{
return "_" + Guid.NewGuid().ToString("N");
}
First, we generate a unique name for the property by using the alphanumeric representation of a GUID (specified by the “N” format string). We prefix the property name with an underscore to ensure a legal identifier.
Then, we register an attached property with the unique name, indicating that its type is IBehavior and that AttachedBehavior owns it. Having all properties owned by the same type hides the use of attached properties completely from behavior authors.
Here is the constructor we called from the Register method:
private readonly DependencyProperty _property;
private readonly Func<DependencyObject, IBehavior> _behaviorFactory;
internal AttachedBehavior(DependencyProperty property, Func<DependencyObject, IBehavior> behaviorFactory)
{
_property = property;
_behaviorFactory = behaviorFactory;
}
Lifecycle
The Behavior field encapsulates the lifecycle of the attached behavior. It has one method, Update, which recalculates a host object’s behavior in response to changes:
private static void OnArgumentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Behavior.Update(d);
}
The handler for dependency property changes in the BooleanVisibility class of part 1
Update manages the association between the host object and an instance of the behavior. This involves three operations:
- Attach – Create a behavior instance and associate it with the host object
- Update – Synchronize the behavior with the current state of the host object
- Detach – Remove the association when the behavior no longer applies to the host object
- In concrete terms, attach and detach refer to setting and clearing the value of the dependency property we generated in the Register call. This discreetly stores the behavior instance directly within the host.
Update is straightforward to implement:
public void Update(DependencyObject host)
{
var behavior = (IBehavior) host.GetValue(_property);
if(behavior == null)
{
TryCreateBehavior(host);
}
else
{
UpdateBehavior(host, behavior);
}
}
private void TryCreateBehavior(DependencyObject host)
{
var behavior = _behaviorFactory(host);
if(behavior.IsApplicable())
{
behavior.Attach();
host.SetValue(_property, behavior);
behavior.Update();
}
}
private void UpdateBehavior(DependencyObject host, IBehavior behavior)
{
if(behavior.IsApplicable())
{
behavior.Update();
}
else
{
host.ClearValue(_property);
behavior.Detach();
}
}
Before attaching or updating the behavior, we call IsApplicable to ensure it is relevant. This is the time to check things like whether dependency properties have values or whether a control is visible. For example, we may not want to attach a behavior to a combo box if it has no items.
Implementers of IBehavior supply the core logic for each of the operations:
public interface IBehavior
{
bool IsApplicable();
void Attach();
void Update();
void Detach();
}
A behavior is assumed to have access to the host object, as shown in the Register call where we pass the host to the constructor of BooleanVisibilityBehavior.
Logic
To minimize friction, we should allow behavior authors to work with a strongly-typed host object. We can enable this by allowing them to specify the host type as a type parameter and maintaining the untyped weak reference ourselves:
public abstract class Behavior<THost> : IBehavior where THost : DependencyObject
{
private readonly WeakReference _hostReference;
protected Behavior(DependencyObject host)
{
if(!(host is THost))
{
throw new ArgumentException("Host is not the expected type", "host");
}
_hostReference = new WeakReference(host);
}
private THost GetHost()
{
return (THost) _hostReference.Target;
}
}
The GetHost method is the bridge between the weak reference and the typed API. We keep it private because derived classes won’t need it; instead, we pass them the host as a parameter in the method for each operation:
protected virtual bool IsApplicable(THost host)
{
return true;
}
protected virtual void Attach(THost host)
{}
protected virtual void Detach(THost host)
{}
protected abstract void Update(THost host);
This is the core API for authoring a new behavior. Only the Update method is abstract; a behavior might not define specific logic for applicability or attaching/detaching, but it exists to respond to changes in the host.
AttachedBehavior calls them from the IBehavior methods, which ensure the host hasn’t been garbage-collected:
public bool IsApplicable()
{
var host = GetHost();
return host != null && IsApplicable(host);
}
public void Attach()
{
var host = GetHost();
if(host != null)
{
Attach(host);
}
}
public void Detach()
{
var host = GetHost();
if(host != null)
{
Detach(host);
}
}
public void Update()
{
var host = GetHost();
if(host != null)
{
Update(host);
}
}
Revisiting BooleanVisibilityBehavior
The AttachedBehavior and Behavior<> types implement most of the mechanics of an attached behavior. They factor away the nitty gritty and leave behavior authors with a straightforward implementation:
private sealed class BooleanVisibilityBehavior : Behavior<UIElement>
{
internal BooleanVisibilityBehavior(DependencyObject host) : base(host)
{}
protected override void Update(UIElement host)
{
host.Visibility = GetValue(host) ? GetWhenTrue(host) : GetWhenFalse(host);
}
}
We declare the host type in the generic parameter to Behavior<>. We accept the host instance in the constructor and simply pass it through to the base class. When it comes time to update the host, it is cast to the generic type and passed to the Update method. We use the accessors for the Value, WhenTrue, and WhenFalse attached properties to determine the host’s new visibility.
(In part 1 of this series, we also listened to changes in the Visibility property and updated the Value property to match. This turned out to be more complex than expected, and so I have left that out of this iteration of the framework. The Attach/Detach methods, normally used to manage event handlers, weren’t necessary in this example.
In the majority of cases, these behaviors will manipulate a control property based on attached properties; two-way binding, a valuable but less applicable scenario, will be covered in a future post.)
Sample Project
This WPF application shows the framework in action:
Attached Behaviors Part 2 Framework.zip
It contains all of the code above and allows you to set the value of the BooleanVisibility.Value attached property on two different text blocks. The first shows when Value is true. The second uses the WhenTrue and WhenFalse attached properties to show when Value is false instead.
Summary
Different attached behaviors have very little that is unique about them. Behavior<> brings these elements to the forefront and, along with AttachedBehavior, takes care of the bookkeeping.
This core system lowers the barrier to entry for new attached behaviors, opening the door for many other ideas. Future posts in this series will cover:
- Binding Visibility to null/not null
- Binding Visibility to an enumeration
- Binding IsEnabled to an enumeration
- Binding radio buttons to an enumeration
- Binding combo boxes and list boxes to an enumeration