Introduction
The ScatterView
in the Surface SDK and Surface Toolkit is a common way to visualize content that can be manipulated freely by users. The problem with it is that the default size that is set for added child elements is almost always too small. While it is possible to set the size when ScatterViewItem
s are added explicitly, there is no flexible way to do this in a modern, data driven application where large parts of the UI are generated automatically behind the scenes.
This article explains a solution to this problem.
The Problem
The ScatterView
control is at its core an ItemsControl
, and will render its children as floating objects that can be rotated, scaled, or moved by users using multi touch. Just like a ListBox
(which is also an ItemsControl
), it will automatically generate containers for its children, and in the case of ScatterView
, these children are always of the type ScatterViewItem
. By default, a ScatterViewItem
will try to calculate a size for itself, but in my experience, it will almost always be wrong. Even setting the width and height of the control inside the ScatterViewItem
won't help.
The only way to explicitly set the initial size of a ScatterViewItem
is to modify its Width
and Height
properties. This is trivially done if the items are created from XAML or from C# code, but what we really need is a way for a control to specify to its parent ScatterViewItem
the size it wishes to be rendered at. This allows the UI to be fully driven from the data in the ViewModels when using the Model-View-ViewModel (MVVM) design.
The Solution
The solution to the problem is to place a control between the ScatterViewItem
and our content. This control would be responsible for providing a way for child content to request an initial size that the ScatterView
will render them at when they are created. While we could put this logic inside each control that would be placed inside a ScatterView
, it doesn't really scale well in a large application. By placing it in a common control, we're not only avoiding code duplication, but we also get the added benefit that we can provide a consistent look for all our items.
In this article, we will give them the appearance of floating windows, complete with title bar and close buttons. But first, let's just define a very simple popup window that provides the sizing functionality we need, and then we can worry about looks later.
<UserControl x:Class="ScatterViewSizingSample.PopupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="http://schemas.microsoft.com/surface/2008">
<Grid Background="White">
<ContentControl x:Name="c_contentHolder"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
</Grid>
</UserControl>
The interesting part above is the ContentControl
which is acting as a host for the actual content. Ideally, it would get its content through data binding, but in order to keep this example simple, we just set it in the constructor:
public PopupWindow(object content)
{
InitializeComponent();
c_contentHolder.Loaded += new RoutedEventHandler(c_contentHolder_Loaded);
c_contentHolder.Content = content;
}
The PopupWindow
control is responsible for looking at the child (i.e., the actual content, which is an UIElement
) and asking it what size it wants. It does this by exposing an attached property called InitialSizeRequest
which the child content is expected to set. The implementation of attached properties follows the boilerplate code that can be generated by Visual Studio:
public static Size GetInitialSizeRequest(DependencyObject obj)
{
return (Size)obj.GetValue(InitialSizeRequestProperty);
}
public static void SetInitialSizeRequest(DependencyObject obj, Size value)
{
obj.SetValue(InitialSizeRequestProperty, value);
}
public static readonly DependencyProperty InitialSizeRequestProperty =
DependencyProperty.RegisterAttached("InitialSizeRequest", typeof(Size),
typeof(PopupWindow), new UIPropertyMetadata(Size.Empty));
Other controls that expect to be hosted inside a ScatterView
can then set this in either XAML or in code (typically, XAML makes most sense):
<UserControl x:Class="ScatterViewSizingSample.FixedSizeChild"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ScatterViewSizingSample"
local:PopupWindow.InitialSizeRequest="300,250"
>
The PopupWindow
will wait until it is fully loaded (since at that time, it will have its visual tree all set up), and then looks at its child content to see if it has this property set.
To traverse the visual tree, I use a utility class called GuiHelpers
which uses VisualTreeHelper
and LogicalTreeHelper
to reliably walk the tree in either direction. I won't explain the details of that code here, but it is included in the sample if anyone is interested in how it works.
private static Size DefaultPopupSize = new Size(300, 200);
private Size CalculateScatterViewItemSize()
{
var presenter = GuiHelpers.GetChildObject<ContentPresenter>(c_contentHolder);
if (presenter == null)
return DefaultPopupSize;
var child = VisualTreeHelper.GetChild(presenter, 0);
if (child == null)
return DefaultPopupSize;
var requestedSize = PopupWindow.GetInitialSizeRequest(child);
if (!requestedSize.IsEmpty
&& requestedSize.Width != 0
&& requestedSize.Height != 0)
{
var borderHeight = this.ActualHeight - c_contentHolder.ActualHeight;
var borderWidth = this.ActualWidth - c_contentHolder.ActualWidth;
return new Size(requestedSize.Width + borderWidth,
requestedSize.Height + borderHeight);
}
else
return DefaultPopupSize;
}
The returned size from this method can then be set directly to the ScatterView
, which can be acquired by walking the visual tree upwards until we find a control of that type:
void c_contentHolder_Loaded(object sender, RoutedEventArgs e)
{
var requestedSize = CalculateScatterViewItemSize();
var svi = GuiHelpers.GetParentObject<ScatterViewItem>(this, false);
if (svi != null)
{
svi.Width = requestedSize.Width;
svi.Height = requestedSize.Height;
}
}
The result is now:
This is all that is needed to get the functionality we want. Controls that will be placed inside a ScatterView
can now specify how large they should be initially, and the user is then free to resize them if they so choose. But I also mentioned that we could use the PopupWindow
to give a nicer look and behavior for our items. Let's add a window border and a button to close them, and while we're at it, let's also add a nice animation to give the appearance of popups magically appearing.
The first step is to update the XAML of the PopupWindow
. We're adding a border around it, with a title bar and a close button.
<UserControl x:Class="ScatterViewSizingSample.PopupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="http://schemas.microsoft.com/surface/2008"
Foreground="Black">
<Border CornerRadius="3" BorderBrush="Black" BorderThickness="2"
Background="DarkGray">
<Border BorderBrush="LightGray" CornerRadius="1"
BorderThickness="1" Background="DarkGray">
<DockPanel LastChildFill="True" >
<Border DockPanel.Dock="Top" >
<Grid>
<TextBlock Text="My Popup" FontWeight="Bold"
VerticalAlignment="Center" Margin="15,0" FontSize="20" />
<s:SurfaceButton Content="Close" HorizontalAlignment="Right"
Margin="3" x:Name="btnClose" Click="btnClose_Click"/>
</Grid>
</Border>
<Border x:Name="border" Margin="15,0,15,15" BorderBrush="#FFC9C9C9"
BorderThickness="2">
<Grid Background="White">
<ContentControl x:Name="c_contentHolder" />
</Grid>
</Border>
</DockPanel>
</Border>
</Border>
</UserControl>
This should give it a look like this:
The next step is to add animation for the width, height, and opacity of the ScatterViewItem
. The width and height will go from zero to their calculated target size, and the opacity will go from 0% to 100%. The animation will be controlled by an easing function that will give it a more natural feeling. (Note: the easing function was added in .NET 4.0, so if you wish to run this on .NET 3.5, just remove those parts; the rest of the animation code is 3.5 compatible):
private void AnimateEntry(Size targetSize)
{
var svi = GuiHelpers.GetParentObject<ScatterViewItem>(this, false);
if (svi != null)
{
IEasingFunction ease = new BackEase
{ EasingMode = EasingMode.EaseOut, Amplitude = 0.3 };
var duration = new Duration(TimeSpan.FromMilliseconds(500));
var w = new DoubleAnimation(0.0, targetSize.Width, duration)
{ EasingFunction = ease };
var h = new DoubleAnimation(0.0, targetSize.Height, duration)
{ EasingFunction = ease };
var o = new DoubleAnimation(0.0, 1.0, duration);
w.Completed += (s, e) => svi.BeginAnimation(ScatterViewItem.WidthProperty, null);
h.Completed += (s, e) => svi.BeginAnimation(ScatterViewItem.HeightProperty, null);
svi.Width = targetSize.Width;
svi.Height = targetSize.Height;
svi.BeginAnimation(ScatterViewItem.WidthProperty, w);
svi.BeginAnimation(ScatterViewItem.HeightProperty, h);
svi.BeginAnimation(ScatterViewItem.OpacityProperty, o);
}
}
The only caveat when working with animations is that unless you explicitly remove the animations after they have completed, the effect of them will remain on the animated property. Normally, this doesn't matter, but since user manipulation of ScatterViewItem
s also modifies the Width
and Height
properties, the user wouldn't be able to resize the items at all.
In the code above, an event handler is hooked up to the Completed
event of each animation, and the handler will effectively remove the animation from that property. The code also sets the values manually before the animation is started, and thanks to the way dependency properties prioritize their sources, that manual value won't be effective until the animation is removed (but it will still be stored in the property).
The Sample Code
This article is accompanied by a small application that demonstrates this functionality. The solution was created using Visual Studio 2010, and is targeting .NET 4.0, but the code itself should be easily adaptable to Visual Studio 2008/.NET 3.5, which is what Microsoft Surface expects.
To build it, you will need to have the Surface Toolkit for Windows Touch installed, since this is where all the Surface controls, including the ScatterView
, is declared.
The application consists of a SurfaceWindow
with a grid containing the ScatterView
, which we will be adding items to, as well as some buttons at the top to add items. To fully show the different ways a ScatterViewItem
is sized, there are three different user controls that can be added to the ScatterView
.
NoSizeChild
- This child demonstrates how the ScatterView
behaves without the functionality explained in this article
FixedSizeChild
- This child demonstrates how to set a fixed initial size for a child in XAML
RandomSizeChild
- This child demonstrates how to dynamically set the initial size for a child when it is created.
Introducing Data Binding
In this sample, there is no data driving the application. In a larger application, the ScatterView
would typically be backed by a ViewModel and all its items bound to an ObservableCollection
. The solution presented above is very suitable for such a design - the fact is that it was designed for that initially, but simplified for the purpose of brevity in this article.
If there is enough interest, I could write a follow-up article showing how this approach is used in an MVVM design (please comment below), but in the meantime, here are some design suggestions:
- Create a ViewModel just for popups (e.g.,
PopupWindowViewModel
) containing properties for the Close command, title, and content. Use a DataTemplate
with a DataType
property for this ViewModel to tell the UI how to render it.
- Create a master ViewModel that has an
ObservableCollection
of PopupWindowViewModel
s that should be displayed in the ScatterView
.
- Create separate ViewModels for each type of content (and a corresponding View to them that gets assigned through typed
DataTemplate
s).
- When adding a popup window, just create a new
PopupWindowViewModel
with its content set to any of the ViewModels from the bullet above, and then add it to the ObservableCollection
.
Points of Interest
My first attempt at addressing this issue was to create my own ScatterViewItem
(and ScatterView
) by inheriting from the original ones. The new ScatterViewItem
s would measure their content using the MeasureOverride
approach and size themselves accordingly. I never managed to make this work in a satisfying way since UIElement
s will measure themselves differently depending on how much area you give them. This caused certain items (notably Images) to become extremely large, while others simply ignored the extra size given to them and just reported their minimum requirements.
I think this approach is an acceptable solution, and it gives the extra benefit of being able to consistently style all items without having to mess with the control template of the ScatterViewItem
. It also makes sense for the designer to set the size explicitly instead of having a computer try to guess it.
History
- v1.0 (9/12/2010) - Initial release.