Monday, January 22, 2007

How small can you go?

In the past, creation of fully sizable user interfaces was a tedious job involving writing complex and bugging "pixel-counting" code to layout controls upon a window resize event. Windows Presentation Foundation (WPF) has made the job of creating such UIs much simpler by including a straightforward layout engine within the framework. But sometimes there is a limit to how small you can resize the UI before it becomes unusable.

One solution is the approach used by the 2007 Microsoft Office System, whereby the ribbon control will disappear from view if the window is resized too small, allowing the document content to fill the space. In this post I'll describe how to implement this effect in WPF applications. I will discuss how to implement dependency properties in your custom controls and the strange world of read-only dependency properties.

As a very simple example we will start with a custom control that is styled with a content area, and a toolbar that when sized below a certain size will be hidden (in my WPF ribbon control this is actually a styled Window, however for simplicity here I will use a control).

public class SizingControl : Control
{
}


Now we will add a "dependency property", a special type of property used for WPF applications that allows additional functionality such as data binding, animations and styling. We define these with the DependencyProperty.Register static method, supplying a property name, property type and owner type. In addition, metadata may be added defining addition information such as default values. In addition we add a standard CLR property of the same name, passing the value to and from the property system with the DependencyObject.GetValue and DependencyObject.SetValue methods. In this case we add a dependency property to specify the size below which the control will display a different UI.



public static readonly DependencyProperty SmallSizeProperty = DependencyProperty.Register("SmallSize", typeof(Size), typeof(SizingControl), new UIPropertyMetadata(new Size(0.0, 0.0)));

public Size SmallSize
{
get
{
return (Size)GetValue(SmallSizeProperty);
}
set
{
SetValue(SmallSizeProperty, value);
}
}


Next we add a read-only dependency property that indicates whether the control is below the specified size. The Windows SDK warns against using read-only dependency properties in many cases, and suggests that such properties should only be used "for state determination". Think of this as properties of the form "IsXXX" (such as IsMouseOver).


To register a read-only dependency property, we use the DependencyProperty.Register.ReadOnly static method, which rather than a DependencyProperty, returns a DependencyPropertyKey. To set the value we can use this with the respective override of DependencyObject.SetValue, however no suitable override of DependencyObject.GetValue exists. In fact, the property system does not allow us to get a value of a dependency property that is marked as read-only. So how do we obtain this value? The answer is that we have to include a private field to contain the property value. Since by its very nature a read-only property may only be set by the owner class, we can always ensure that the value in the field is synchronized with that of the dependency property. To avoid writing code that breaks this rule, I tend to encapsulate this logic into a private property setter (note that property setters and getters with different accessibility is a feature of C# 2.0).



private bool isSmall;

public static readonly DependencyPropertyKey IsSmallPropertyKey = DependencyProperty.RegisterReadOnly("IsSmall", typeof(bool), typeof(SizingControl), new UIPropertyMetadata(false));

public bool IsSmall
{
get
{
return isSmall;
}
private set
{
if (value != isSmall)
{
isSmall
= value;
SetValue(IsSmallPropertyKey, value);
}
}
}


Now all is required is to override the OnRenderSizeChanged event of our control, and determine whether the control is small or not.



private void UpdateIsSmall(Size newSize)
{
IsSmall
= (newSize.Width < SmallSize.Width || newSize.Height < SmallSize.Height);
}

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
// Calculate whether the window size is smaller than the specified values
UpdateIsSmall(sizeInfo.NewSize);

// Call the base method
base.OnRenderSizeChanged(sizeInfo);
}


The XAML below shows how to use the control to hide a toolbar when the window is too small. We style the control with the desired content, and set a trigger on the IsSmall property to show or hide the toolbar as required.



<Window x:Class="SizingApplication.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="SizingApplication" Height="300" Width="300" xmlns:local="clr-namespace:SizingApplication" Background="Silver">
<Window.Resources>
<Style x:Key="MySizingRegion" TargetType="local:SizingControl">
<Setter Property="SmallSize" Value="200,200"/>

<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SizingControl">
<DockPanel>
<ToolBar x:Name="toolBar" DockPanel.Dock="Top" Height="50">
<Label VerticalAlignment="Center">My ToolBar</Label>
</ToolBar>
<Border Margin="10" Background="White"/>
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsSmall" Value="True">
<Setter TargetName="toolBar" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>

<Grid>
<local:SizingControl Style="{StaticResource MySizingRegion}"/>
</Grid>
</Window>


The full code for this post is here.

4 comments:

Anonymous said...

Nice article!
I'm also trying to implement a Ribbon using WPF and I'm happy to be able to find bits of information here and there. My analysis is that Office 2007 is using an early version of WPF.

Subscribed.

Pascal

Andy Wilkinson said...

I'm pleased you found the article useful Pascal. It's always nice to hear that people are reading my blog.

I've got a more general method for adding sizing behaviour in WPF that I'll try to post about soon (via "value converters" and "multivalue converters"). Also I'll try to put up some pictures of my Office 2007 style UI so far.

As for what Office uses, I'm not sure. I know the Vista window manager uses the same rendering technologies as WPF (but with a different layout system on top) so maybe Office does the same? I'd be surprised if Office was too WPF based at this stage though.

Andy

Anonymous said...

Until now, I didn't found any interesting custom implementation of the Ribbon control. Most implementations are based on MFC (!), Win32 or .Net Framework 2. As of Office 07, Spy++ is not able to find any internal window for the Ribbon: a clear indication that the Ribbon is custom-painted. By looking carefuly at the animation used in Office, you will see that almost everything seems to be do-able very efficiently with WPF.

I'm not saying that Office is directly build with WPF, but I can imagine that at the Office Team may have ripped an early implementation and started their own branch of the source code from here. On the other hand, Microsoft is big enough to implement the same technology twice.

For now on, I'm struggling to build an efficient, re-usable Ribbon library. I didn't find much information on Petzold/Sells books or online documentation (not about the ribbon, but about reusable components.) I'll try to post some articles (or at least pictures of the advance of my work).

Cheers,
Pascal

Andy Wilkinson said...

I've not had a look at the Office Ribbon in Spy++ but if it is all in one HWND then there must be some rendering engine backing it all up. The WPF renderer would definately share a lot of similar code.

As for information on reusable components I agree there isn't much available. I guess this reflects the relative immaturity of WPF as a platform. Most information available concentrates on the basics, rather than the higher level concepts such as building controls. My biggest source of ideas at the moment is just seeing how the base WPF controls work (looking at the object model, pulling out templates in Expression Blend, etc.).