Building C# custom controls in WinRT Metro

This article describes how to build custom WinRT Metro controls in C# and XAML, with the Developer Preview version of Visual Studio 11. WPF, Silverlight, and WinRT Metro have two ways to roll your own controls: custom controls, and user controls. User controls are composite controls that are created by dragging and dropping other controls onto a XAML surface. They are nothing more than content controls that are optimized for design-time use. User controls are typically for sharing within an application, but not with other applications. Custom controls on the other hand, are created by defining a class that derives -directly or indirectly- from Control, together with a definition of a default style. Custom controls are typically hosted in control libraries, and shared between multiple applications. Custom controls are much more flexible in terms of reuse, styling, templating, and theming.

For this article I created a simplified version of the Slider control. It's called SimpleSlider. It has a Minimum, Maximum, and Value property. Visually it only consists of a moving part -the thumb- and a colored rectangle of which the width is bound to the Value property:

I first created a project in .NET 4.0 WPF with Visual Studio 10. Then I tried to implement the same functionality in a .NET 4.5 Metro-style app. Here are screenshots of both versions:

 

Creation of the control

For WPF there is a WPF Custom Control Library project template. It creates a custom control derived from the most common base class: Control. You might want to change that into a more appropriate class, like ItemsControl or Selector. RangeBase would have been the ideal base class for a Slider, but I wanted to build everything from scratch, so I kept the default.

The Visual Studio template assigns the DefaultStyleKey dependency property through an OverrideMetaData call in a static constructor. It also creates a Themes folder with a generic.xaml with an (almost) empty default control template for the new user control. The project's assembly information gets a ThemeInfo attribute that contains the locations where that default style will be searched when the control needs to be displayed.

That's how it goes in Silverlight and WPF. For Metro style apps there is NO such project template - there's not even a class template for a custom control. The reason is that things like DefaultStyleKey, ThemeInfo and generic.xaml are not supported (yet?). So you have to create the SimpleSlider class and its generic.xaml manually. To inject the style file into the application, add it as a resource dictionary in app.xaml:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Generic.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

 

Adding Bindable Properties

By deriving from Control, a custom control inherits a set of useful dependency properties. These are properties to which the user of the control can databind: things like Background, FontSize, and Template. You can add custom dependency properties yourself - Visual Studio has a code snippet for that: propdp. It generates the dependency property registration for you:

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", "Object", typeof(SimpleSlider).FullName, new PropertyMetadata(0.0, OnValueChanged));
        
public double Value
{
    get { return (double)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

The PropertyMetadata allows you to define the default value and the method that should be called when the property's value is changed. When the Value property of the SimpleSlider is changed, we're going to redraw it:

private static void OnValueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
    var customSlider = (SimpleSlider)dependencyObject;
    customSlider.UpdateControls();
}

Contrary to WPF and Silverlight, the Register method takes strings instead of types. This is just temporary: Microsoft will regain compatibility with Silverlight and WPF in the upcoming beta of WinRT.

In the Developer Preview you need to specify 'Object' as the property type to make the bindings work, but that's also just a temporary bug.

Styling

Defining the default style for your custom control is not different from current technologies. You just define a style with the corresponding target type in a resource dictionary. Here's the full definition of the default style for the simple slider:

<Style TargetType="ctrl:SimpleSlider">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ctrl:SimpleSlider">
                <Grid>
                    <Border Height="8"
                            VerticalAlignment="Stretch"
                            Background="LightGray" />
                    <Canvas Margin="0"
                            MinHeight="8">
                        <Rectangle x:Name="PART_Rectangle"
                                    Height="8"
                                    Fill="Yellow" />
                        <Thumb x:Name="PART_Thumb"
                                Width="8"
                                Height="8" />
                    </Canvas>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I would suggest to NOT use TemplateBinding in a style definition, since this doesn't seem to work yet. The following code, where I replaced the Border in the template by a Rectangle with its Width bound to the actual width of the control, does not work in the Developer Preview:

<Rectangle Height="8"
           Width="{TemplateBinding ActualWidth}"
           VerticalAlignment="Stretch"
           Fill="LightGray" />

If you're looking for inspiration about styles: all the default styles for WPF and Silverlight controls are on MSDN. You can find the full style for the WPF Slider here. The WinRT Metro styles are not published yet, but if you open the Basic Controls Sample, you'll find a light_generic.xaml file that has all the information. You'll immediately see why my control is called 'Simple' Slider...

Templating

The user of your custom control should be able to decide for 200% on how your control should look like in his application. But of course he needs to respect some basic requirements: a slider will always have a moveable part (the Thumb) and an indication of its Value (the Rectangle). The required elements in a style are by convention named with a 'PART_' prefix. The code behind file also exposes these elements' name and type through a TemplatePart attribute:

[TemplatePart(Name = ThumbPartName, Type = typeof(Thumb))]
[TemplatePart(Name = RectanglePartName, Type = typeof(Rectangle))]
public class SimpleSlider : Control
{
    private const string ThumbPartName = "PART_Thumb";
    private const string RectanglePartName = "PART_Rectangle";

    // ...
}

This indicates to potential control re-stylers that the code-behind relies on these parts. So if you override the default style, you should make sure that it contains elements with the expected name and type.

The style -default or custom- is typically applied in the OnApplyTemplateCore method, that's Metro's version of the OnApplyTemplate method that we know from Silverlight and WPF. As a developer of custom controls, you should wrap the template part manipulation in defensive code. After all, you never know what template you will be dealing with at run-time. Use GetTemplateChild to find the template part you're interested in, since WPF's Template.Find does not exist in Silverlight and Metro:

protected override void OnApplyTemplateCore()
{
    base.OnApplyTemplateCore();

    this.thumb = this.GetTemplateChild(ThumbPartName) as Thumb;
    if (this.thumb != null)
    {
        this.thumb.DragDelta += this.Thumb_DragDelta;
    }
    
    // ...
}

All changes to a dependency property fire the INotifyPropertyChanged event. Unfortunately, in the Developer Preview something goes wrong in the central binding mechanism when the ElementName property is used. The following code displays the initial slider value correctly in a textblock. But when the thumb is moved, the textbox just disappears:

<TextBlock Text="{Binding Path=Value, ElementName=SimpleSlider}" />

If you bring a viewmodel in the equation and use it as datacontext for slider and textbox, then everything works as expected. This looks like overkill, but as a die-hard MVVM developer I can live with that. Here's the -working- code for the value binding of the pacman-shaped slider:

<TextBlock Text="{Binding SliderValue}" />

Overriding a style

To override the default style, you just need to provide a new one, e.g. through XAML:

<ctrl:SimpleSlider.Style>
    <Style TargetType="ctrl:SimpleSlider">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ctrl:SimpleSlider">
                    <!-- Your template here -->
                    <!-- ... -->
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ctrl:SimpleSlider.Style>

Just make sure you use the required template parts.

Conclusion

There are a couple of glitches in the Developer Preview, but in general the WinRT Metro framework already has most of the foundations to build reusable custom control libraries. In a couple of weeks there will be a new release: not a true beta release, but a so-called  'consumer preview'. I can't wait to get my hands on it...

Code

Here's the full code, the WinRT version was built with the Visual Studio 11 Developer Preview:
WPF: U2UConsult.WPF.SimpleSlider.zip (24,81 kb)
WinRT: U2UConsult.WinRT.SimpleSlider.zip (34,84 kb)

Enjoy!

Afterthought

I realize that it was unfair to compare WinRT Metro directly to WPF. The WinRT Metro mechanisms are much closer to Silverlight than they are to WPF. WinRT Metro currently feels like a stripped-down version of Silverlight (which itself is a stripped-down version of WPF). At first, I found that a bit strange. After all, the WinRT runtime is not restrained by footprint size: it doesn't have to be smaller than the Flash plugin. It's also not constrained by browser and operating system compatibility: it doesn't have to run in Opera on a Windows Server 2000, nor in Safari on a Mac. On the other hand, WinRT is targeted to run on cheap (well, hopefully) tablets powered by ARM processors, the 32-bit cores that drive smartphones, MP3-players, and the Nintendo DS. That counts as a constraint.


Hello ObservableVector, goodbye ObservableCollection

WinRT, the new Windows 8 runtime for Metro applications, introduces a new interface for collection change notification. IObservableVector<T> replaces ye olde INotifyCollectionChanged. The ObservableCollection class still exists, you can continue to use it. Unfortunately its collection change events are ignored by the WinRT framework. I can assure you that this will give cross-platform developers a serious headache - or worse. Don't say I didn't warn you:

The IObservableVector<T> interface is defined, but the framework does not contain an implementation yet. There's no built-in collection that raises the new VectorChanged events. The fresh cocoon framework on CodePlex contains an implementation of ObservableVector<T>. Unsurprisingly it's a light-weight wrapper around an IList<T>. The class is described here.

Here's my own version of the class - I just adapted it to my property change notification mechanism:

namespace Mvvm
{
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using Windows.Foundation.Collections;
    using Windows.UI.Xaml.Data;

    /// <summary>
    /// IObservableVector<T> implementation.
    /// </summary>
    public class ObservableVector<T> : Collection<T>, INotifyPropertyChanged, IObservableVector<T>
    {
        // *** Events ***

        public event PropertyChangedEventHandler PropertyChanged;
        public event VectorChangedEventHandler<T> VectorChanged;

        // *** Constructors ***

        public ObservableVector()
            : base()
        {}

        public ObservableVector(IList<T> list)
            : base(list)
        {}

        // *** Protected Methods ***   

        protected override void ClearItems()
        {
            base.ClearItems();
            this.PropertyChanged.Raise(this, o => o.Count);
            this.PropertyChanged.Raise(this, o => o.Items);
            this.OnVectorChanged(CollectionChange.Reset, 0);
        }

        protected override void InsertItem(int index, T item)
        {
            base.InsertItem(index, item);
            this.PropertyChanged.Raise(this, o => o.Count);
            this.PropertyChanged.Raise(this, o => o.Items);
            this.OnVectorChanged(CollectionChange.ItemInserted, (uint)index);
        }

        protected override void RemoveItem(int index)
        {
            base.RemoveItem(index);
            this.PropertyChanged.Raise(this, o => o.Count);
            this.PropertyChanged.Raise(this, o => o.Items);
            this.OnVectorChanged(CollectionChange.ItemRemoved, (uint)index);
        }

        protected override void SetItem(int index, T item)
        {
            base.SetItem(index, item);
            this.PropertyChanged.Raise(this, o => o.Items);
            this.OnVectorChanged(CollectionChange.ItemChanged, (uint)index);
        }

        // *** Event Handlers ***

        protected void OnVectorChanged(CollectionChange collectionChange, uint index)
        {
           this.OnVectorChanged(new VectorChangedEventArgs(collectionChange, index));
        }

        protected virtual void OnVectorChanged(IVectorChangedEventArgs e)
        {
            if (this.VectorChanged != null)
                this.VectorChanged(this, e);
        }

        // *** Private Sub-classes ***

        private class VectorChangedEventArgs : IVectorChangedEventArgs
        {
            // *** Fields ***

            private readonly CollectionChange collectionChange;
            private readonly uint index;

            // *** Constructors ***

            public VectorChangedEventArgs(CollectionChange collectionChange, uint index)
            {
                this.collectionChange = collectionChange;
                this.index = index;
            }

            // *** Properties ***

            public CollectionChange CollectionChange
            {
                get
                {
                    return this.collectionChange;
                }
            }

            public uint Index
            {
                get
                {
                    return this.index;
                }
            }
        }
    }
}

I consider this temporary code - I assume that next versions of WinRT will have a native version of ObservableVector<T>.

I built a small sample application around to demonstrate the usage of the class in a MVVM application: all the work is done through data and command bindings. The app just manages two collections of Dragons: 'All Dragons' and 'Favorites'. The selected Dragon in each ListBox can be moved to the other collection by clicking the buttons in the middle. Here's how the app looks like:

By the way, in the current version of WinRT -Developer Preview- the collection changed events are only handled if T is object, so you have to define the collection as follows:

public ObservableVector<object> Dragons { get; set; }

For any other type -like ObservableVector<string> or ObservableVector<Dragon>- the change events will be simply ignored by the binding mechanism. That's another headacheUndecided.

You use the class exactly the same way as good old ObservableCollection, e.g. as ItemsSource to a an ItemControl:

<ListBox DataContext="{Binding}"
         ItemsSource="{Binding Dragons}"
         SelectedItem="{Binding SelectedDragon, Mode=TwoWay}" />

Here's the full source code, it's built with Visual Studio 11 Developer Preview: U2UConsult.WinRT.ObservableVector.Sample.zip (47,83 kb)

Enjoy!


Download the U2U brochure

Download Brochure

Receive the U2U Newsletter. Submit your email address:
 
 


 


Search

rss  RSS

Tags

Archive

Blogroll