In my previous article I introduced a Floating Control for Windows Store apps, and hinted that it could be rewritten as a Behavior. Well, that’s exactly what I did. This article describes the Floating Behavior: it allows a ContentControl to be dragged around the screen through mouse or touch, while optionally keeping it within its parent and/or on screen. Here’s the Behavior in action; any similarity with the app from my previous article *is* intended:
Blend Behaviors are very popular in WPF and Silverlight. They are classes that encapsulate interactive behavior that can be attached to visual elements and that is generally implemented by registering event handlers to that associated element. Behaviors were missing in WinRT until they were introduced with Windows 8.1 and Visual Studio 2013. For a more detailed introduction to Behaviors in WinRT 8.1 I refer to this article by Mark Smith.
In WinRT a Behavior is a class that implements the IBehavior interface. The classic Behavior<T> does not exist on this platform. But don’t worry: if you want to upgrade one of your legacy behaviors you can easily resurrect Behavior<T> yourself. There’s an example by Fons Sonnemans right here.
Here’s the class declaration for FloatingBehavior:
/// <summary>
/// Adds Floating Behavior to a ContentControl.
/// </summary>
public class FloatingBehavior : DependencyObject, IBehavior
{
// ...
}
Apart from the mandatory interface implementations, the class comes with the exact same dependency properties (IsBoundByParent and IsBoundByScreen) and position calculations as the Floating Control.
Here’s how to connect the Behavior to a ContentControl in XAML:
<ContentControl>
<interactivity:Interaction.Behaviors>
<behaviors:FloatingBehavior IsBoundByParent="True" />
</interactivity:Interaction.Behaviors>
<!-- Content Here -->
</ContentControl>
Before you can define or use a Behavior, you need to reference the Behaviors SDK that comes with Blend:
When a Behavior is attached to a XAML element, the Attach method is called. Herein we check if the Behavior is attached to a ContentControl. If that’s the case, we register an event handler for the Loaded event; if not, we complain:
private ContentControl contentControl;
/// <summary>
/// Attaches to the specified object.
/// </summary>
public void Attach(DependencyObject associatedObject)
{
this.contentControl = associatedObject as ContentControl;
if (this.contentControl == null)
{
throw new Exception("Floating Behavior only applies to ContentControl.");
}
else
{
this.contentControl.Loaded += ContentControl_Loaded;
}
}
It’s in that Loaded event that we register the core handlers for ManipulationDelta and SizeChanged. These two actually define the Floating behavior, and are explained the previous article. Before we hook these event handlers, we need to cram a Canvas and a Border between the ContentControl and its parent. These are necessary for the position adjustment calculations. The FloatingControl carries these decorations in its style template, but the FloatingBehavior needs to do the plumbing in C#. And it’s actually a tedious operation: you have to pull the ContentControl out of its parent, wrap it in a Border in a Canvas, and then plug it back into the parent - which may retrigger the Loaded event. The pull-out and plug-in operations unconveniently depend on the parent’s type. I added implementations for Panel, ContentControl, and Border [Why is Border not a ContentControl?] but I may be missing some potential host controls here:
/// <summary>
/// Handles the Loaded event of the ContentControl.
/// </summary>
private void ContentControl_Loaded(object sender, RoutedEventArgs e)
{
// Make sure this only runs once.
// The Loaded event is also fired when the contentcontrol is moved in the Visual Tree
this.contentControl.Loaded -= ContentControl_Loaded;
var parent = this.contentControl.Parent;
if (parent is Panel)
{
var panel = parent as Panel;
int i = panel.Children.IndexOf(this.contentControl);
panel.Children.Remove(this.contentControl);
panel.Children.Insert(i, this.Decorated);
}
else if (parent is ContentControl)
{
var cc = parent as ContentControl;
cc.Content = null;
cc.Content = this.Decorated; ;
}
else if (parent is Border)
{
var border = parent as Border;
border.Child = null;
border.Child = this.Decorated;
}
else
{
throw new Exception("Unexpected parent, please call a code monkey.");
}
this.frame = GetClosestParentWithSize(this.border);
// No parent.
if (this.frame == null)
{
// We probably never get here.
return;
}
this.frame.SizeChanged += Floating_SizeChanged;
}
Wrapping the control is simple. The Decorated property returns a Canvas:
/// <summary>
/// Initializes and returns the decorated control.
/// </summary>
private Canvas Decorated
{
get
{
// Canvas
var canvas = new Canvas();
canvas.Height = 0;
canvas.Width = 0;
canvas.VerticalAlignment = VerticalAlignment.Top;
canvas.HorizontalAlignment = HorizontalAlignment.Left;
// Border
this.border = new Border();
this.border.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY | ManipulationModes.TranslateInertia;
this.border.ManipulationDelta += this.Border_ManipulationDelta;
// Move Canvas properties from control to border.
Canvas.SetLeft(border, Canvas.GetLeft(this.contentControl));
Canvas.SetLeft(this.contentControl, 0);
Canvas.SetTop(border, Canvas.GetTop(this.contentControl));
Canvas.SetTop(this.contentControl, 0);
// Move Margin to border.
this.border.Padding = this.contentControl.Margin;
this.contentControl.Margin = new Thickness(0);
// Connect the dots
this.border.Child = this.contentControl as UIElement;
canvas.Children.Add(this.border);
return canvas;
}
}
When the Behavior is removed from the ContentControl, the Detach method from IBehavior is called. That’s where we remove the event handlers:
/// <summary>
/// Detaches this instance from its associated object.
/// </summary>
public void Detach()
{
this.contentControl.Loaded -= ContentControl_Loaded;
this.frame.SizeChanged -= Floating_SizeChanged;
}
The floating behavior only applies to ContentControl so I’m ignoring the AssociatedObject property in my code since it is not strongly typed. Here’s the implementation, just for the sake of completeness:
/// <summary>
/// Gets the <see cref="T:Windows.UI.Xaml.DependencyObject" /> to which the <seealso cref="T:Microsoft.Xaml.Interactivity.IBehavior" /> is attached.
/// </summary>
/// <remarks>Not used. We prefer the strongly type contentControl field.</remarks>
public DependencyObject AssociatedObject
{
get
{
return this.contentControl;
}
}
Here’s the Behavior in action when the app is resized. the restrained controls nicely stay within the green rectangle, or on screen:
I personally prefer the clean implementation of ‘floating’ as a Control over this Behavior version. But this example certainly proves that the new Windows 8.1 Store app Behaviors are capable of doing more complex things than you might have expected.
Here’s the source code, it was written in Visual Studio 2013 for Windows 8.1. The Behavior is in its own project: U2UC.WinRT.FloatingBehaviorSample.zip (736.5KB)
Enjoy!
Diederik