Click here to Skip to main content
Click here to Skip to main content

Custom Sized ScatterViewItems

, 9 Dec 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Demonstrates how to set the initial size of ScatterViewItems based on the content's requested size.

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 ScatterViewItems 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);
}

// Using a DependencyProperty as the backing store for InitialSizeRequest.
// This enables animation, styling, binding, etc...
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.

// In case the child didn't specify a requested size, fallback to this size
private static Size DefaultPopupSize = new Size(300, 200);
private Size CalculateScatterViewItemSize()
{
    // Get the part of the ContentControl that hosts the child
    var presenter = GuiHelpers.GetChildObject<ContentPresenter>(c_contentHolder);
    if (presenter == null)
        return DefaultPopupSize;
    // It seems it's safe to assume the ContentPresenter will always only have one child 
    // and that child is the visual representation of the content of c_contentHolder.
    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)
    {
        // Calculate how much this PopupWindow is adding around the content
        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:

Restyled popup window

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);
        
        // Remove the animation after it has completed so that its possible to
        // manually resize the scatterviewitem
        w.Completed += (s, e) => svi.BeginAnimation(ScatterViewItem.WidthProperty, null);
        h.Completed += (s, e) => svi.BeginAnimation(ScatterViewItem.HeightProperty, null);
        // Set the size manually, otherwise once the animation is removed the size
        // will revert back to the minimum size
        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 ScatterViewItems 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 PopupWindowViewModels 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 DataTemplates).
  • 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 ScatterViewItems would measure their content using the MeasureOverride approach and size themselves accordingly. I never managed to make this work in a satisfying way since UIElements 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

isaks
Software Developer ABB
Sweden Sweden
My name is Isak Savo and I work as a Research Engineer at ABB Corporate Research in Västerås, Sweden. My work is focused around user experience which includes a lot of prototyping of new solutions for user interfaces and user interaction.
 
While I have a background in C programming in a Linux environment, my daily work is mostly spent in Windows using C# and WPF.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 1 PinmemberMember 106222954-May-14 8:48 
Questionhi PinmemberMember 1007103610-Aug-13 0:17 
QuestionNeed help to insert text dynamically Pinmembersazzadur15-Jul-11 0:37 
AnswerRe: Need help to insert text dynamically Pinmemberisaks15-Jul-11 6:41 
GeneralRe: Need help to insert text dynamically Pinmembersazzadur15-Jul-11 7:09 
GeneralRe: Need help to insert text dynamically Pinmemberisaks7-Aug-11 21:51 
GeneralRe: Need help to insert text dynamically Pinmembersazzadur22-Jul-11 6:24 
GeneralRe: Need help to insert text dynamically Pinmemberisaks7-Aug-11 21:45 
Try setting HorizontalAlignment="Stretch" and VerticalAlignment="Stretch" on your Polygon. That should make it expand itself when the parent grid is resized.
 
Another option is to put the polygon inside a Viewbox.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 10 Dec 2010
Article Copyright 2010 by isaks
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid