Custom Controls in Windows Phone 7

Recently, I gave a talk at the Chicago Windows Phone Developer Group on building custom controls using XAML templates.  The focus of the talk was to understand the importance of the user experience in a mobile application and how using custom controls can greatly augment the experience.

One of the nicest thing about XAML is the ability to deeply customize controls.  Control Templates can be overridden easily and with all matter of design attributes, it practically is a designers dream (Disclaimer: I am not a designer).

This also extends to custom controls.  Creating a custom control is easy, consider the following code:

public class StatePicker : Control
{
    public override void OnApplyTemplate()
    {
    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Actually, the OnApplyTemplate method is not required by the Control class, but it will play an important role in our example.  Right now, with this class defined, we can provide the following XAML:

<custom:StatePicker x:Name="StatePicker" />

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Now, the control isn’t especially useful because it has no visual appearance.  But we can easily give it a visual appearance by defining its control template.

<Style TargetType="custom:StatePicker">
    <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}" />
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}" />
    <Setter Property="HorizontalContentAlignment" Value="Left" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="custom:StatePicker">
                <Button Background="{TemplateBinding Background}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        FontFamily="{TemplateBinding FontFamily}"
                        Foreground="{TemplateBinding Foreground}"
                        Height="72"
                        HorizontalContentAlignment="{TemplateBinding  HorizontalContentAlignment}"
                        Name="PickerButton" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

By defining this template we are effectively creating a control that encapsulates a Button, we have styled the control to make it look like a TextBox.  This template must now be APPLIED to the control.  You guessed it, the OnApplyTemplate method is a hook into this process.  This is the implementation of OnApplyTemplate that I am using:

        public override void OnApplyTemplate()
        {
            if (_chooserButton != null)
                _chooserButton.Click -= OpenStatePickerPage;

            // apply the template
            base.OnApplyTemplate();

            // wireup the button from the template
            _chooserButton = GetTemplateChild("PickerButton") as ButtonBase;

            // wireup the event
            if (_chooserButton != null)
            {
                _chooserButton.Click += OpenStatePickerPage;
            }
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Important here is the call to base.OnApplyTemplate.  If you do not call this, the call to GetTemplateChild will return null because the control you are looking for is in the template you have no applied.  If you notice above, we gave a name to the button declaration in the template.

OpenStatePicker is responsible for showing the actual picking UI.  Since space is very constrained on mobile devices, don’t be afraid of using a second page to actually carry out an operation on a large set of data.  Your users will be grateful for it, provided it is kept seamless and they don’t even realize they are changing pages.  Ideally, your users should see this as a since control. Many Microsoft controls employ exactly this, and they do it so well that very few users even realize they are being taken to a second page.

To show the actual UI, we simply navigate to the new page using the RootFrame (the frame which contains the application).

        private void OpenStatePickerPage(object sender, RoutedEventArgs e)
        {
            var frame = Application.Current.RootVisual as PhoneApplicationFrame;
            frame.Navigated += frame_Navigated;

            if (_selectedState == null)
                rootFrame.Navigate(new Uri("/StatePickerUI.xaml",
                             UriKind.RelativeOrAbsolute));
            else
                rootFrame.Navigate(
                       new Uri(string.Format("/StatePickerUI.xaml?ab={0}", 
                         _selectedState.Abbreviation), UriKind.RelativeOrAbsolute));
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

One of the most important aspects here is going to be the wireup of the Navigated event from the frame.  This speaks to that temporary navigation that will be enacted.  For the rest of this we are navigating to the StatePickerUI page, passing a parameter if a pre-existing selection already exists.

Our StatePickerUI page will be pretty simple:

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <tk:LoopingSelector ItemSize="143,130"
                            Width="150" Name="StateLoopingSelector">
            <tk:LoopingSelector.ItemTemplate>
                <DataTemplate>
                    <Grid Height="125" Width="140"
                          Background="{StaticResource PhoneChromeBrush}">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="0.736*"/>
                            <RowDefinition Height="0.264*"/>
                        </Grid.RowDefinitions>

                        <TextBlock Style="{StaticResource PhoneTextTitle1Style}"
                            HorizontalAlignment="Center" VerticalAlignment="Center"
                                   Text="{Binding Abbreviation}" Margin="0" />
                        <TextBlock Margin="0" Text="{Binding Name}"
                                   VerticalAlignment="Bottom" Grid.Row="1" Height="25"
                                   Style="{StaticResource PhoneTextSmallStyle}" />
                    </Grid>
                </DataTemplate>
            </tk:LoopingSelector.ItemTemplate>
        </tk:LoopingSelector>
    </Grid>

    <phone:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsVisible="True" IsMenuEnabled="False">
            <shell:ApplicationBarIconButton
                x:Name="CancelButton"
                IconUri="/Toolkit.Content/ApplicationBar.Cancel.png" Text="cancel"
                Click="CancelButton_Click" />
            <shell:ApplicationBarIconButton
                x:Name="AcceptButton"
                IconUri="/Toolkit.Content/ApplicationBar.Check.png" Text="accept"
                Click="AcceptButton_Click" />
        </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Basically, this turns out to look like this:

2012-05-05_0958

We need the application bar so we allow the user to make a selection.  The key things with the LoopingDataSelector primitive is to define a data source.  The class which represents the data source must implement the ILoopingSelectorDataSource interface.  This interface describes three methods, a property, and an event.

  • object GetNext(object)
    • Loads the next object in the data source relative to another.  This is done because the user can swipe the list up and down for an infinite amount.  Your selection logic should be circular
  • object GetPrevious(object)
    • Same as above, just does previous
  • object SelectedItem
    • This is what was actually selected by the user (generally its rendered in the center)
  • event SelectionChanged – When the user makes a selection different from what is previous selected, this event is fired

I wont bore you with the implementations of these methods, rather check out the zipped source that can be downloaded at the bottom.

Once this is in place you need to establish a way to get the selected state from the PickerUI and to the StatePicker control that is on the page.  This is where we come back to the Navigated event.  Here is the code:

private void rootFrame_Navigated(object sender, NavigationEventArgs e)
{
    if (_isOpen)
    {
        // we need to close
        CloseStatePickerPage(_statePicker);

        var frame = Application.Current.RootVisual as PhoneApplicationFrame;
        rootFrame.Navigated -= rootFrame_Navigated;
        _isOpen = false;
    }
    else
    {
        _statePicker = e.Content as IPicker;
        if (_statePicker == null)
             throw new Exception("Some craziness is happening");

        _isOpen = true;     // mark the picker as open
    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

For this example, if the PickerUI is not open, we want to store a reference to the PickerUI (that is e.Content).  To simplify the code, we create a user interface (IPicker) which allows us to extract the selected item from the PickerUI, an instance of the State class in this case.

The reason for this is because we do NOT have access to the PickerUI content when we are coming back from the PickerUI, only when we are going to it.  This is why we must so closely control the “state” of the control.

Also important here is the removal of the Navigated event handler from the rootFrame.  Remember that we are employing “temporary navigation rules” within the “control”.  A control can be made up of many different XAML pages depending the intent.  Thus we need to ensure that we can properly control “state” within the context of the control.  However, when we leave that context, we cannot leave the Navigated event handler in place, less it possibly interfere with functionality in the rest of the application.

Once we have the selected State instance, we can process it however, we like:

        private void CloseStatePickerPage(IPicker statePicker)
        {
            if (statePicker.SelectedItem != null)
            {
                _selectedState = statePicker.SelectedItem;
                _chooserButton.Content = string.Format("{0} ({1})",
                           _selectedState.Name, _selectedState.Abbreviation);
            }
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Feel free to download the full source code, available from Skydrive:

 

Final Thoughts

Lets be honest, mobile applications, even mobile web applications, are not web applications, nor should they be treated and designed as such.  A mobile experience must be crafted to be more personal and intimate then the generic web application user experience which must have a wider appeal.  Mobile environments constrain what can be fit on a screen thus making it even more critical that developers understand how to determine exactly what they need.

Custom controls represent a way to combine existing controls or create some altogether new. In our example, we created more of a composite control as opposed to a full on new control, but the principles are the same.  Thanks the flexibility provided to use with XAML Control Templates, new controls can be very easily designed and implemented.

2 thoughts on “Custom Controls in Windows Phone 7

  1. I see this has been moved already – thanks. Note that usually it is better to have title “on Windows Phone” rather than “on Windows Phone 8” and use the platform version categories to explain what versions the article is true for. I might also include a note in the introduction that an article is “relevant from windows phone 8”. The reason is that there will at some point be WP9 and it is likely this article will still be accurate.

    http://www.agileinfoways.com/technical-expertise/mobile-applications-development/windows-mobile/

    The exception is of course when you know that an article is only relevant for a particular version and won't be updated – so for example articles on using the Bing maps in WP7 would have WP7 in the title.

    Like

Leave a comment