Diederik Krols

The XAML Brewer

A Floating Control for Windows 8 Store apps

Windows 8 Store apps need to run on a huge number of screen resolutions. That makes positioning your controls not always an easy task. So why not let the end user decide where a control should be placed? This article describes how to build a XAML and C# ContentControl that can be dragged around (and off) the screen by using the mouse or touch. The control comes with dependency properties to optionally keep it on the screen or within the rectangle occupied by its parent control. These boundaries apply not only when the control is manipulated, but also when its parent resizes (e.g. when you open multiple apps or when the screen is rotated).

Here’s a screenshot of the attached sample app. The main page contains some instances of the so-called Floating control. Two of these are bound to their parent in the visual tree -the yellow rectangle-, one is bound by the screen, and the remaining one is entirely free to go where you send it:

floating

I created the Floating control as a custom control that inherits from ContentControl, but I think it could be built as a behavior too. Here’s how to use it in your XAML:

<controls:Floating IsBoundByParent="True">
    <!-- Your content comes here -->
</controls:Floating>
<controls:Floating IsBoundByScreen="True">
    <!-- Your content comes here -->
</controls:Floating>

The default style of the Floating control is defined in the Themes\Generic.xaml file:

<!-- Floating Control Style -->
<Style TargetType="local:Floating">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:Floating">
                <!-- This Canvas never covers other controls -->
                <Canvas Background="Transparent"
                        Height="0"
                        Width="0"
                        VerticalAlignment="Top"
                        HorizontalAlignment="Left">
                    <!-- This Border handles the dragging -->
                    <Border x:Name="PART_Border"
                            ManipulationMode="TranslateX, TranslateY, TranslateInertia"  >
                        <ContentPresenter />
                    </Border>
                </Canvas>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The heart of the control is a ContentPresenter that will contain whatever you put in it. It is hosted in a Border that responds to translation manipulations with inertia. That Border moves around within a Canvas that constitutes the outside of the Floating control. That Canvas has a zero height and width, so that it doesn’t cover other controls (e.g. other Floating controls within the same parent).

A Canvas doesn’t clip its content to its bounds, so the Border doesn’t really care about its parent being sizeless: it can be dragged around to everywhere. Unless we apply some restrictions: the Floating control comes with the boundary properties IsBoundByParent and IsBoundByScreen. These are defined as dependency properties:

/// <summary>
/// A Content Control that can be dragged around.
/// </summary>
[TemplatePart(Name = BorderPartName, Type = typeof(Border))]
public class Floating : ContentControl
{
    private const string BorderPartName = "PART_Border";

    public static readonly DependencyProperty IsBoundByParentProperty =
        DependencyProperty.Register("IsBoundByParent", typeof(bool), typeof(Floating), new PropertyMetadata(false));

    public static readonly DependencyProperty IsBoundByScreenProperty =
        DependencyProperty.Register("IsBoundByScreen", typeof(bool), typeof(Floating), new PropertyMetadata(false));

    private Border border;

    /// <summary>
    /// Initializes a new instance of the <see cref="Floating"/> class.
    /// </summary>
    public Floating()
    {
        this.DefaultStyleKey = typeof(Floating);
    }

    /// <summary>
    /// Gets or sets a value indicating whether the control is bound by its parent size.
    /// </summary>
    public bool IsBoundByParent
    {
        get { return (bool)GetValue(IsBoundByParentProperty); }
        set { SetValue(IsBoundByParentProperty, value); }
    }

    /// <summary>
    /// Gets or sets a value indicating whether the control is bound by the screen size.
    /// </summary>
    public bool IsBoundByScreen
    {
        get { return (bool)GetValue(IsBoundByScreenProperty); }
        set { SetValue(IsBoundByScreenProperty, value); }
    }
}

The control’s main job is to calculate the physical position of the Border against its parent Canvas. When the control is moved or when the parent resizes, it will adjust the Canvas.Left and Canvas.Top attached properties of the Border. An alternative approach would be to apply and configure a translation to that Border.

In the OnApplyTemplate we look for the Border in the style template, and register an event handler for ManipulationDelta:

protected override void OnApplyTemplate()
{
    // Border
    this.border = this.GetTemplateChild(BorderPartName) as Border;
    if (this.border != null)
    {
        this.border.ManipulationDelta += this.Border_ManipulationDelta;
    }
    else
    {
        // Exception
        throw new Exception("Floating Control Style has no Border.");
    }

    this.Loaded += Floating_Loaded;
}

In that same method we also apply some adjustments to drastically simplify the calculations. The Floating control may be hosted in a Canvas with a Top and Left, or it may be defined with a Margin around it. Since we’re controlling the position of the Border, not the Floating, I decided to let the Border take over these settings. Canvas properties are stolen from the Floating control, and the Margin outside the Floating is transformed into a Padding inside the Border:

// Move Canvas properties from control to border.
Canvas.SetLeft(this.border, Canvas.GetLeft(this));
Canvas.SetLeft(this, 0);
Canvas.SetTop(this.border, Canvas.GetTop(this));
Canvas.SetTop(this, 0);

// Move Margin to border.
this.border.Padding = this.Margin;
this.Margin = new Thickness(0);

When the control is loaded, we look up the parent to hook an event handler for the SizeChanged event:

private void Floating_Loaded(object sender, RoutedEventArgs e)
{
    FrameworkElement el = GetClosestParentWithSize(this);
    if (el == null)
    {
        return;
    }

    el.SizeChanged += Floating_SizeChanged;
}

Observe that we don’t just look for a resize of the control itself or its direct parent. That’s because these could have an actual height and width of zero – and hence would be ignored by SizeChanged. That seems to happen to Grid and Canvas controls –typical parents for a Floating- very often. So we’re actually looking for the closest parent with a real size:

/// <summary>
/// Gets the closest parent with a real size.
/// </summary>
private FrameworkElement GetClosestParentWithSize(FrameworkElement element)
{
    while (element != null && (element.ActualHeight == 0 || element.ActualWidth == 0))
    {
        element = element.Parent as FrameworkElement;
    }

    return element;
}

When the Border is moved around, we calculate its desired position, and then adjust it so it stays within the boundaries:

private void Border_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    var left = Canvas.GetLeft(this.border) + e.Delta.Translation.X;
    var top = Canvas.GetTop(this.border) + e.Delta.Translation.Y;

    Rect rect = new Rect(left, top, this.border.ActualWidth, this.border.ActualHeight);
    AdjustCanvasPosition(rect);
}

When the parent is resized, we apply the same logic to the current position of the Border:

private void Floating_SizeChanged(object sender, SizeChangedEventArgs e)
{
    var left = Canvas.GetLeft(this.border);
    var top = Canvas.GetTop(this.border);

    Rect rect = new Rect(left, top, this.border.ActualWidth, this.border.ActualHeight);
    AdjustCanvasPosition(rect);
}

If one or both of the boundary properties is set, then we may need to apply a correction to the Canvas.Top and Canvas.Left of the Border. That’s what the following methods do:

/// <summary>
/// Adjusts the canvas position according to the IsBoundBy* properties.
/// </summary>
private void AdjustCanvasPosition(Rect rect)
{
    // No boundaries
    if (!this.IsBoundByParent && !this.IsBoundByScreen)
    {
        Canvas.SetLeft(this.border, rect.Left);
        Canvas.SetTop(this.border, rect.Top);

        return;
    }

    FrameworkElement el = GetClosestParentWithSize(this);

    // No parent
    if (el == null)
    {
        // We probably never get here.
        return;
    }

    var position = new Point(rect.Left, rect.Top); ;

    if (this.IsBoundByParent)
    {
        Rect parentRect = new Rect(0, 0, el.ActualWidth, el.ActualHeight);
        position = AdjustedPosition(rect, parentRect);
    }

    if (this.IsBoundByScreen)
    {
        var ttv = el.TransformToVisual(Window.Current.Content);
        var topLeft = ttv.TransformPoint(new Point(0, 0));
        Rect parentRect = new Rect(topLeft.X, topLeft.Y, Window.Current.Bounds.Width - topLeft.X, Window.Current.Bounds.Height - topLeft.Y);
        position = AdjustedPosition(rect, parentRect);
    }

    // Set new position
    Canvas.SetLeft(this.border, position.X);
    Canvas.SetTop(this.border, position.Y);
}

/// <summary>
/// Returns the adjusted the topleft position of a rectangle so that is stays within a parent rectangle.
/// </summary>
private Point AdjustedPosition(Rect rect, Rect parentRect)
{
    var left = rect.Left;
    var top = rect.Top;

    if (left < -parentRect.Left)
    {
        left = -parentRect.Left;
    }
    else if (left + rect.Width > parentRect.Width)
    {
        left = parentRect.Width - rect.Width;
    }

    if (top < -parentRect.Top)
    {
        top = -parentRect.Top;
    }
    else if (top + rect.Height > parentRect.Height)
    {
        top = parentRect.Height - rect.Height;
    }

    return new Point(left, top);
}

Here’s what happens when the app is resized or rotated, the bound Floating controls remain inside the box and/or on screen:

floating_resized

floating_rotated

Here’s the full source code. The Floating control is immediately reusable, since it lives in its own project: U2UC.WinRT.FloatingSample.zip (1.9MB)

Enjoy!

Diederik

Comments are closed