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.
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 UIElement
s 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 ProxyUIElement
s.
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)
{
}
}
}
}
}
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
History