Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Layout to Layout Transitions

0.00/5 (No votes)
27 Nov 2012 1  
Layout to layout transitions made easy

Introduction

This article describes an implementation of WPF panel that enables layout to layout transitions easily using XAML only. LayoutPanel, the name of the panel, does not contain its own layout logic but is able to arrange its children as almost all existing panels do. The best way to see LayoutPanel functionality is to watch the video of sample applications I’ve posted on YouTube.    

Demo application video

Background

First time I saw layout to layout animations demo in "WPF Feature montage" I was really excited how it looked and worked.  I found the idea very promising for the next generation of applications with great design and usability. While being impressed by demo I didn’t like the way it was implemented very much. Making visual elements clones and moving them from panel to panel back and forth seemed to me a bit like a hack. 

Sometime later I understood the main requirements for layout to layout switching API that in turn helped me to identify an implementation idea: 

With these statements in mind I have implemented the first version of LayoutPanel back in 2008.  The idea behind is so simple and elegant that it is hard to imagine how could it have been any different . It is completely based on Delegation pattern and described in more details in implementation section. Later being inspired by the great article of Fredrik Bornander  
Animated WPF Panels” I added animation support to the implemented panel. Unfortunately, I didn’t manage to describe and publish an implementation at that time. Still hope the described implementation will be useful to someone. 

How to Use

The panel described in the article has very simple API for switching layouts.    

  • IsAnimated – Gets/sets whether AnimatedLayoutPanel animates transition between layouts. The default value is true.  
  • Panel – Gets/sets an ItemPanelTemplate that is used as current layout logic. Default value is StackPanel.

Basic code for using AnimatedLayoutPanel is shown below.  

<Controls:AnimatedLayoutPanel> 
    <Controls:LayoutPanel.Panel>
          <ItemsPanelTemplate>
               <Canvas/>
          </ItemsPanelTemplate>
    </Controls:LayoutPanel.Panel>
</Controls:AnimatedLayoutPanel>

Implementation

The way how panel layouts it’s children is controlled by single dependency property: 

public static readonly DependencyProperty PanelProperty =
        DependencyProperty.Register("Panel", typeof(ItemsPanelTemplate), typeof(LayoutPanel),
        new FrameworkPropertyMetadata(new ItemsPanelTemplate(new FrameworkElementFactory(typeof(StackPanel))),
            FrameworkPropertyMetadataOptions.AffectsMeasure |
            FrameworkPropertyMetadataOptions.AffectsArrange,
            PanelChanged));
 
private static void PanelChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs args)
{
    LayoutPanel layoutPanel = sender as LayoutPanel;
 
    if (layoutPanel != null)
    {
    var newPanel = LoadPanelTemplate((ItemsPanelTemplate)args.NewValue);
    layoutPanel.m_Panel = newPanel;
    layoutPanel.InitializeChildren();
    }
}
 
public ItemsPanelTemplate Panel
{
    get
    {
        return (ItemsPanelTemplate)GetValue(PanelProperty);
    }
    set
    {
        SetValue(PanelProperty, value);
    }
}   

This property is of ItemPanelTemplate type to encapsulate the internal panel used for layout logic to avoid direct Children collection modification as we need to keep its visual tree in sync with host panel.

As mentioned above the main implementation idea is to delegate measure and arrange calls to internal Panel. This may seem to be not so easy task as UIElements may have only single logical and visual parent. And here ProxyUIElement class comes in handy, allowing us to measure real children in virtual visual tree of panel specified.

private class ProxyUIElement : FrameworkElement
{
    private UIElement m_Element;
 
    public ProxyUIElement(UIElement element)
    {
        m_Element = element;
    }
 
    public UIElement Element
    {
        get { return m_Element; }
    }
 
    protected override Size MeasureOverride(Size constraint)
    {
        Element.Measure(constraint);
        return Element.DesiredSize;
    }
} 

Children collections are being synchronized using the following code: 

protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
    base.OnVisualChildrenChanged(visualAdded, visualRemoved);
    if (visualAdded != null)
    {
        var props = GetAllAttachedProperties();
        foreach (var d in props)
        {
            d.AddValueChanged(visualAdded, AttachedPropertyValueChanged);
        }
 
        var child = (UIElement) visualAdded;
        int index = Children.IndexOf(child);
        var proxyElement = CreateProxyUIElement(child);
        PanelInternal.Children.Insert(index, proxyElement);
    }
    if (visualRemoved != null)
    {
        var props = GetAllAttachedProperties();
        foreach (var d in props)
        {
            d.RemoveValueChanged(visualRemoved, AttachedPropertyValueChanged);
        }
        var proxyElement = GetProxyUIElement(visualRemoved); 
        PanelInternal.Children.Remove(proxyElement);
        SetProxyUIElement(visualRemoved, null);
    }
}

During Measure phase children are being measured in virtual visual tree.  

protected override Size MeasureOverride(Size availableSize)
{
    PanelInternal.Measure(availableSize);
    return PanelInternal.DesiredSize;
}   

As a next step LayoutPanel arranges its children using coordinates of corresponding ProxyUIElements.

protected override Size ArrangeOverride(Size finalSize)
{
    PanelInternal.Arrange(new Rect(finalSize));
    foreach (ProxyUIElement child in PanelInternal.Children)
    {
        Point point = child.TranslatePoint(new Point(), PanelInternal);
        child.Element.Arrange(new Rect(point, child.RenderSize));
    }
    return PanelInternal.RenderSize;
}

Dealing with attached dependency properties

For the most WPF panels, except the simpliest ones, layout logic can be controlled by attached dependency properties. To support this kind of scenario LayoutPanel synchronizes all attached dependency properties for its children and corresponding proxy elements.  

var props = GetAllAttachedProperties();
foreach (var d in props)
{
    d.AddValueChanged(visualAdded, AttachedPropertyValueChanged);
}
 

private ProxyUIElement CreateProxyUIElement(UIElement child)
{
    var proxyElement = new ProxyUIElement(child);
    SetProxyUIElement(child, proxyElement);
    var props = GetAllAttachedProperties();
    foreach (var d in props)
    {
        d.SetValue(proxyElement, d.GetValue(child));
    }
    return proxyElement;
}
 
private void AttachedPropertyValueChanged(object sender, EventArgs args)
{
    var element = (UIElement) sender;
    var proxyElement = GetProxyUIElement(element);
    if (proxyElement != null)
    {
        var props = GetAllAttachedProperties();
        foreach (var d in props) //
        {
            d.SetValue(proxyElement, d.GetValue(element));
        }
    }
}

As there is no way to know whether specific attached dependency property is used by current layout logic, panel implementation keeps  in sync all the attached properties registered.  

private static IEnumerable<DependencyPropertyDescriptor> GetAllAttachedProperties()
{
    if (ms_AttachedProperties != null)
    {
        return ms_AttachedProperties;
    }
 
    ms_AttachedProperties = new List<DependencyPropertyDescriptor>();
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        foreach (Type t in assembly.GetTypes())
        {
            foreach (FieldInfo fi in t.GetFields(BindingFlags.DeclaredOnly
                | BindingFlags.Static | BindingFlags.Public))
            {
                if (fi.FieldType == typeof(DependencyProperty))
                {
                    DependencyProperty dp = (DependencyProperty)fi.GetValue(null);
                    try
                    {
                        DependencyPropertyDescriptor dpd =
                            DependencyPropertyDescriptor.FromProperty(dp, t);
 
                        if (dpd != null && dpd.IsAttached && !dpd.IsReadOnly)
                        {
                            ms_AttachedProperties.Add(dpd);
                        }
                    }
                    catch (ArgumentException)
                    {
                        // there was a problem obtaining the
                        // DependencyPropertyDescriptor?
                    }
                }
            }
        }
    }
 
    return ms_AttachedProperties;
}

Adding animation support

The animation to LayoutPanel was added using  elegant solution described in “Animated WPF Panels” article.  

public class AnimatedLayoutPanel : LayoutPanel
{
    private DispatcherTimer animationTimer;
    private DateTime lastArrange = DateTime.MinValue;
 
    public IArrangeAnimator Animator { get; set; }
 
    public static readonly DependencyProperty IsAnimatedProperty = 
        DependencyProperty.Register("IsAnimated", 
        typeof (bool), typeof (AnimatedLayoutPanel), new PropertyMetadata(true));
 
    public AnimatedLayoutPanel()
        : this(new FractionDistanceAnimator(0.25), TimeSpan.FromSeconds(0.05))
    {
    }
 
    public AnimatedLayoutPanel(IArrangeAnimator animator, TimeSpan animationInterval)
    {
        animationTimer = AnimationBase.CreateAnimationTimer(this, animationInterval);
        Animator = animator;
    }
 
    protected override Size ArrangeOverride(Size finalSize)
    {
        finalSize = base.ArrangeOverride(finalSize);
 
        if (IsAnimated)
        {
            AnimationBase.Arrange(Math.Max(0, 
              (DateTime.Now - lastArrange).TotalSeconds), this, Children, Animator);
        }
        lastArrange = DateTime.Now;
 
        return finalSize;
    }
 
    public bool IsAnimated
    {
        get { return (bool) GetValue(IsAnimatedProperty); }
        set { SetValue(IsAnimatedProperty, value); }
    }
}

Demo 1

 

In Demo 1, I have tried to resemble the functionality of WPF feature montage demo as much as possible by using only XAML.  To show that implementation is not hard coded only for Panels supplied within Framework libraries I have added to the list RadialPanel found somewhere in the Internet.    

<Grid>
    <Grid.Resources>
        <x:Array x:Key="Panels" Type="ItemsPanelTemplate">
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Vertical"/>
            </ItemsPanelTemplate>
            <ItemsPanelTemplate>
                <WrapPanel ItemHeight="50" ItemWidth="50"/>
            </ItemsPanelTemplate>
            <ItemsPanelTemplate>
                <Controls:RadialPanel/>
            </ItemsPanelTemplate>
            <ItemsPanelTemplate>
                <DockPanel/>
            </ItemsPanelTemplate>
            <ItemsPanelTemplate>
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="2*"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                </Grid>
            </ItemsPanelTemplate>
            <ItemsPanelTemplate>
                <Canvas/>
            </ItemsPanelTemplate>
        </x:Array>
    </Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="80"/>
    </Grid.ColumnDefinitions>
    <Controls:AnimatedLayoutPanel x:Name="LayoutPanel" 
          Panel="{Binding ElementName=PanelsListBox, Path=SelectedItem.Tag}">
    </Controls:AnimatedLayoutPanel>
    <StackPanel Orientation="Vertical" Grid.Column="1">
        <CheckBox Content="Animate" 
                IsChecked="{Binding ElementName=LayoutPanel, Path=IsAnimated}"/>
        <Button Content="Add Child" Click="Add_Click"/>
        <ListBox SelectedIndex="0" x:Name="PanelsListBox" Height="200">
            <ListBoxItem Content="Stack Horixontal" Tag="{Binding Source={StaticResource Panels}, Path=[0]}"/>
            <ListBoxItem Content="Stack vertical" Tag="{Binding Source={StaticResource Panels}, Path=[1]}"/>
            <ListBoxItem Content="WrapPanel" Tag="{Binding Source={StaticResource Panels}, Path=[2]}"/>
            <ListBoxItem Content="RadialPanel" Tag="{Binding Source={StaticResource Panels}, Path=[3]}"/>
            <ListBoxItem Content="DockPanel" Tag="{Binding Source={StaticResource Panels}, Path=[4]}"/>
            <ListBoxItem Content="Grid" Tag="{Binding Source={StaticResource Panels}, Path=[5]}"/>
        </ListBox>
    </StackPanel>

</Grid>

Demo 2

Demo 2 presents more sophisticated scenario where current layout logic is selected based on some conditions. As LayoutPanel.PanelProperty is dependency property it may use all existing WPF functionality such as animations, triggers and styles out of the box.  

To change layout logic based on DataTrigger a style for LayoutPanel is defined.  

<Style TargetType="Controls:LayoutPanel">
    <Setter Property="Panel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="70"/>
                        <ColumnDefinition Width="5*"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                </Grid>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBox},
                      Path=SelectedItem}" Value="{x:Null}">
            <Setter Property="Panel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <UniformGrid Columns="2"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>
</Style>

By default UniformGrid is used for implemented menu layout. If there is selection in ListBox layout changes to Grid with two columns, selected item moves to second column and changes its appearance. 

<ListBox.ItemContainerStyle>
    <Style TargetType="ListBoxItem">
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <Setter Property="VerticalContentAlignment" Value="Stretch"/>
        <Setter Property="Grid.Row" Value="{Binding Row}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ListBoxItem">
                    <ContentPresenter Content="{TemplateBinding Content}" 
                       ContentTemplate="{TemplateBinding ContentTemplate}"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Grid.Column" Value="1"/>
                <Setter Property="Grid.Row" Value="0"/>
                <Setter Property="Grid.RowSpan" Value="6"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ListBox.ItemContainerStyle> 

Limitations

  • It is almost impossible to support virtualized panels.
  • Before it can be used in professional looking application  a more fine grained control over animation should be implemented (for example as it is in SwitchPanel from Blendables Layout Mix)  
  • Do not even ask whether it is possible to use LayoutPanel as internal panel for layout logic Smile | <img src=  

History

  • Current version is 1.0.  

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here