Rotating Elements in XAML While Maintaining Proper Flow

I recently had to lay out some text vertically stacked on top of each other in Windows 8, similar to how tabs in Visual Studio are laid out.

image

The obvious way to do that would be to first place the texts in a stack panel, and then rotate them like so:

<StackPanel>
    <TextBlock Text="Text 1">
        <TextBlock.RenderTransform>
            <RotateTransform Angle="90" />
        </TextBlock.RenderTransform>
    </TextBlock>
    <TextBlock Text="Text 2">
        <TextBlock.RenderTransform>
            <RotateTransform Angle="90" />
        </TextBlock.RenderTransform>
    </TextBlock>
    <TextBlock Text="Text 3">
        <TextBlock.RenderTransform>
            <RotateTransform Angle="90" />
        </TextBlock.RenderTransform>
    </TextBlock>
</StackPanel>

This is what it looks like without the RotateTransform:

image

And after adding rotation:

image

Notice how the text is now outside the containing StackPanel, and overlapping each other? So what happened? The problem is that RenderTransform is applied AFTER the layout cycle occurs so StackPanel has no way of placing these elements, since it already did it’s job prior to rotating the text. We’ll get back to how to resolve this, but let’s first cover the layout cycle, which consists of two steps: Measure and Arrange.

In the Measure step, each TextBlock is measured. This is basically the step where the parent control – in this case the StackPanel – tells each TextBlock “If you have [x,y] space, how much of that would you like to have?”. It does this by calling TextBlock.Measure(Size) on each of them. The TextBlock then reports back the size it would like to have using the .DesiredSize property. You will notice that controls’ DesiredSize property will always return (0,0) until the Measure step has run. Usually for TextBlocks it would report back the size of the text. If the text doesn’t fit within the width that the StackPanel provided and TextWrapping was enabled on the TextBlock, the TextBlock might choose to break the text and report a taller height instead, so it can keep inside the width it was provided with.

The next step is the Arrange step. This occurs after all children has been measured, and the StackPanel here decides how much space it will provide to each control. While the TextBlocks provided a certain DesiredSize, they might not actually get that much space – that’s all up to the parent control – for instance for a Grid’s columns and rows with auto and * sizes, the measure step helped it determine how much space each row and column will be, and then applies that to each element during arrange. Arrange is done by calling .Arrange(Rect) on each element, providing them with a rectangle to place itself within.

So back to our problem: How can we use this knowledge to get the layout cycle to proper place my rotated textblocks?

Well first of all, when we rotate an element 90 degrees, the width of the text becomes the height, and vice-versa. So if we were to “swap” the width and height during Arrange, we should be able to prevent the overlapping and ensure that there’s enough width, errr height for each TextBlock so they don’t overlap. During the arrange step, we can ensure that the elements gets placed right and doesn’t end up outside the containing control.

To do that, the first thing we’ll do is create a new control. Add a new TemplatedControl to your project:

image

A new class inheriting from Control will be created, as well as a new \Themes\Generic.xaml template (if you already have a Generic.xaml file, the following will be added):

Let’s get rid of the Border in this template, and add a simple ContentControl instead:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:RotateSample">

    <Style TargetType="local:RotateContentControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:RotateContentControl">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
<ContentControl x:Name="Content" Content="{TemplateBinding Content}" />
</ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>

Next let’s add a Content dependency property to our class so that we can bind to the content. We’ll also add a Content property to the class, to signify that anything that is used as content inside this control in XAML is meant to be assigned to the Content property. Our control now looks like this:

[Windows.UI.Xaml.Markup.ContentProperty(Name="Content")]
public sealed class RotateContentControl : Control
{
    public RotateContentControl()
    {
        this.DefaultStyleKey = typeof(RotateContentControl);
    }

    public object Content
    {
        get { return (object)GetValue(ContentProperty); }
        set { SetValue(ContentProperty, value); }
    }

    public static readonly DependencyProperty ContentProperty =
        DependencyProperty.Register("Content", typeof(object), typeof(RotateContentControl), null);      
}

Now if we were to run the app using this control, it’ll basically be the same as using a ContentControl.

<local:RotateContentControl>
    <TextBlock Text="Text 1" FontSize="32" Margin="5" />
</local:RotateContentControl>

Of course this is not much fun, so let’s first use the OnApplyTemplate to grab the content and apply the rotation to the content:

private ContentControl m_Content;
private const double rotation = 90;
protected override void OnApplyTemplate()
{
    m_Content = GetTemplateChild("Content") as ContentControl;
    if (m_Content != null)
    {
        m_Content.RenderTransform = new RotateTransform() { Angle = rotation };
    }
    base.OnApplyTemplate();
}

If you run the sample now, we’ll basically be back to where we started with the texts offset and placed outside the parent container. You can see that the StackPanel is highlighted below with the size it thinks it needs to be to hold the TextBlocks, which doesn’t match the actual size of the TextBlocks:

image

So let’s first override the Measure step and swap width and heights:

protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize)
{
    if (m_Content != null)
    {
        m_Content.Measure(new Size(availableSize.Height, availableSize.Width));
        return new Size(m_Content.DesiredSize.Height, m_Content.DesiredSize.Width);
    }
    else
        return base.MeasureOverride(availableSize);
}

You’ll now see the following happen – notice how the height and width is now correct for the StackPanel if the TextBlocks were rendered in the right place, but we start seeing clipping on the TextBlocks:

image

This happens because the ArrangeStep still uses the unswapped width/height and causes clipping. Let’s next override the Arrange and swap width and height here as well:

protected override Size ArrangeOverride(Size finalSize)
{
    if (m_Content != null)
    {
        m_Content.Arrange(new Rect(new Point(0, 0), 
new Size(finalSize.Height, finalSize.Width))); return finalSize; } else return base.ArrangeOverride(finalSize); }

And the result we get is:

image

Now the text are not overlapping any longer nor are they clipped, but we still get them placed outside the parent StackPanel. This is because the rotation happens around the upper left corner and pushes the text out, as illustrated here:

image

Luckily the fix is easy because the Arrange step allows us to specify where to place the element as well. We basically have to move the TextBlock over to the left by the height of the text, so instead of specifying (0,0) for the rectangle corner, we use (width,0), so our Arrange looks like this:

protected override Size ArrangeOverride(Size finalSize)
{
    if (m_Content != null)
    {
        m_Content.Arrange(new Rect(new Point(finalSize.Width, 0), new Size(finalSize.Height, finalSize.Width)));
        return finalSize;
    }
    else
        return base.ArrangeOverride(finalSize);
}

And our controls now flows correctly within the StackPanel:

image

It also plays nice with other controls and respects alignments:

image

If you want to rotate the content –90 degrees, the offset in arrange changes slightly to:

m_Content.Arrange(new Rect(new Point(0, finalSize.Height), 
new Size(finalSize.Height, finalSize.Width)));

We could make this a property on our control, so you can easily change direction on the fly. We’ll add a new Direction enumeration and a DependencyProperty that triggers Arrange when it changes:

public RotateDirection Direction
{
    get { return (RotateDirection)GetValue(DirectionProperty); }
    set { SetValue(DirectionProperty, value); }
}

public static readonly DependencyProperty DirectionProperty =
    DependencyProperty.Register("Direction", typeof(RotateDirection),
typeof(RotateContentControl), new PropertyMetadata(RotateDirection.Down, OnDirectionPropertyChanged)); public static void OnDirectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as RotateContentControl).InvalidateArrange(); //Trigger reflow }

We’ll also use remove the RenderTransform setting from OnApplyTemplate, and instead set it in ArrangeOverride, so that now looks like this:

protected override Size ArrangeOverride(Size finalSize)
{
    if (m_Content != null)
    {
        m_Content.RenderTransform = new RotateTransform() { Angle = (int)this.Direction };
        if (Direction == RotateDirection.Down)
            m_Content.Arrange(new Rect(new Point(finalSize.Width, 0), 
new Size(finalSize.Height, finalSize.Width))); else if (Direction == RotateDirection.Up) m_Content.Arrange(new Rect(new Point(0, finalSize.Height),
new Size(finalSize.Height, finalSize.Width))); return finalSize; } else return base.ArrangeOverride(finalSize); }

So here’s what that looks like in the designer:

image

That’s it! Below is the entire source code including support for 0 and 180 degree rotations as well:

using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace RotateSample
{
    [Windows.UI.Xaml.Markup.ContentProperty(Name="Content")]
    public sealed class RotateContentControl : Control
    {
        private ContentControl m_Content;

        public RotateContentControl()
        {
            this.DefaultStyleKey = typeof(RotateContentControl);
        }

        protected override void OnApplyTemplate()
        {
            m_Content = GetTemplateChild("Content") as ContentControl;
            base.OnApplyTemplate();
        }

        protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize)
        {
            if (m_Content != null)
            {
                if (((int)Direction) % 180 == 90)
                {
                    m_Content.Measure(new Windows.Foundation.Size(availableSize.Height, availableSize.Width));
                    return new Size(m_Content.DesiredSize.Height, m_Content.DesiredSize.Width);
                }
                else
                {
                    m_Content.Measure(availableSize);
                    return m_Content.DesiredSize;
                }
            }
            else
                return base.MeasureOverride(availableSize);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (m_Content != null)
            {
                m_Content.RenderTransform = new RotateTransform() { Angle = (int)this.Direction };
                if (Direction == RotateDirection.Up)
                    m_Content.Arrange(new Rect(new Point(0, finalSize.Height),
                                      new Size(finalSize.Height, finalSize.Width)));
                else if (Direction == RotateDirection.Down)
                    m_Content.Arrange(new Rect(new Point(finalSize.Width, 0), 
                                      new Size(finalSize.Height, finalSize.Width)));
                else if (Direction == RotateDirection.UpsideDown)
                    m_Content.Arrange(new Rect(new Point(finalSize.Width, finalSize.Height), finalSize));
                else
                    m_Content.Arrange(new Rect(new Point(), finalSize));
                return finalSize;
            }
            else
                return base.ArrangeOverride(finalSize);
        }


        public object Content
        {
            get { return (object)GetValue(ContentProperty); }
            set { SetValue(ContentProperty, value); }
        }

        public static readonly DependencyProperty ContentProperty =
            DependencyProperty.Register("Content", typeof(object), typeof(RotateContentControl), null);

        public enum RotateDirection : int
        {
            Normal = 0,
            Down = 90,
            UpsideDown = 180,
            Up = 270
        }

        public RotateDirection Direction
        {
            get { return (RotateDirection)GetValue(DirectionProperty); }
            set { SetValue(DirectionProperty, value); }
        }

        public static readonly DependencyProperty DirectionProperty =
            DependencyProperty.Register("Direction", typeof(RotateDirection),
            typeof(RotateContentControl), new PropertyMetadata(RotateDirection.Down, OnDirectionPropertyChanged));

        public static void OnDirectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if(((int)e.OldValue) % 180 == ((int)e.NewValue) % 180)
                (d as RotateContentControl).InvalidateArrange(); //flipping 180 degrees only changes flow not size
            else
                (d as RotateContentControl).InvalidateMeasure(); //flipping 90 or 270 degrees changes size too, so remeasure
        }
    }
}

Note: While this article was written for Windows Store apps, these concepts apply directly to Silverlight, WPF and Windows Phone as well, albeit they already provide controls in the Toolkit (LayoutTransformer) to handle this.

Comments (5) -

  • This is a great description of how to customize the control to achieve the result and I'm glad you wrote this.

    But... is there a reason you didn't want to use LayoutTransform? This seems to achieve the same effect.

    (Pasting XAML - hope it's not stripped out)

    <StackPanel>
        <TextBlock Text="Text 1">
            <TextBlock.LayoutTransform>
                <RotateTransform Angle="90"/>
            </TextBlock.LayoutTransform>
        </TextBlock>
        <TextBlock Text="Text 2">
            <TextBlock.LayoutTransform>
                <RotateTransform Angle="90" />
            </TextBlock.LayoutTransform>
        </TextBlock>
        <TextBlock Text="Text 3">
            <TextBlock.LayoutTransform>
                <RotateTransform Angle="90" />
            </TextBlock.LayoutTransform>
        </TextBlock>
    </StackPanel>


    John
  • John: Yes there is a VERY good reason I don't use that property: it doesn't exist in WinRT. That property is only available in WPF
  • So to say that "these concepts apply directly to Silverlight, WPF and Windows Phone as well" is, while true, a little misleading. It's about 100x simpler in WPF thanks to LayoutTransform. (This is just one of a long list of many things that are horribly painful in WinRT compared to WPF. I hope they bring it into alignment, rather than inventing yet another XAML platform 2 years from now like they have done every 2 years since WPF fixed...)
  • ugh. Clearly I'm still in WPF-land. Why would they leave that out??!!
  • Ian: I see your point, but RenderTransform does the exact same thing in WPF, and the functionality of Arrange and Measure are again the exact same thing as well. This blogpost is mainly about how Arrange/Measure and the layout steps works.

    John: It's never been in Silverlight, and WinRT is based on SL, not WPF.

    While I do agree WPF has a lot of fancy nice-to-have features, it's also incredibly bloated and slow (I cross-compile A LOT of Silverlight code to WPF and SL outperforms it every time). I could imagine leaving out LayoutTransform skips a step on all the elements during the layout cycle and that has performance implications for a large UI tree, and as this blogpost demonstrates, it's possible to get by without LayoutTransform, so it's not a need-to-have, but only a sometimes-nice-to-have.

Pingbacks and trackbacks (1)+

Add comment