WP7 Contrib – Customising the DateTime Picker

As you may already be aware there are two controls in the WP7 toolkit that provide the standard interactions and UI for selecting time and a date. Personally these are 2 of my favourite controls mainly due to the transitions and simple UI. However….. What happens when we are building an application that needs you to enter credit card details ? Or you only want a subset of the provided controls i,e, a Month and a Year ? Unfortunately at the moment AFAIK  there is no way of doing this with the provided controls, its just not possible using Xaml either via the Style or the Template. We need to dig in a little deeper and find out what controls are actually powering these pickers. Well you don’t have to go too far before you find something called the Looping Selector control, the control provides an Item Template so that we can if we want add our own Data Template if so desired.

So why would you want to do this ? Well for us we need to provide an experience where the user of the app is prompted to enter in their credit card details. It seems only sensible to use the standard UI metaphor for picking a Date or a Time on WP7. As I found out this is just not possible and a level of customisation is required in order to achieve the interactions that we want along with the right experience.

This problem can be divided into two main pieces; data we will need to provided some subclassed data sources for day, month and year; UI we need to change the looping selectors visibility and do some work around hooking up the controls with data.

In the WP7 Contrib View project you can locate the DateTime Picker folder where the data sources are defined to support the picker. The main thing that we here is to reuse the same data source classes that are defined in the toolkit however we have changed the accessor to be public so that we can seem where we are using the control. The code below is here for completion and your viewing pleasure.

The Day DataSource

namespace WP7Contrib.View.Controls.DateTimePicker
{
    using System;

    public class DayDataSource : DataSource
    {
        // Methods
        /// <summary>
        /// Gets the relative to.
        /// </summary>
        /// <param name="relativeDate">The relative date.</param>
        /// <param name="delta">The delta.</param>
        /// <returns></returns>
        protected override DateTime? GetRelativeTo(DateTime relativeDate, int delta)
        {
            int num = DateTime.DaysInMonth(relativeDate.Year, relativeDate.Month);
            return new DateTime(relativeDate.Year, relativeDate.Month, ((((num + relativeDate.Day) - 1) + delta) % num) + 1);
        }
    }
}

The Month DataSource

namespace WP7Contrib.View.Controls.DateTimePicker
{
    using System;

    public class MonthDataSource : DataSource
    {
        // Methods
        /// <summary>
        /// Gets the relative to.
        /// </summary>
        /// <param name="relativeDate">The relative date.</param>
        /// <param name="delta">The delta.</param>
        /// <returns></returns>
        protected override DateTime? GetRelativeTo(DateTime relativeDate, int delta)
        {
            int num = 12;
            int month = ((((num + relativeDate.Month) - 1) + delta) % num) + 1;
            return new DateTime(relativeDate.Year, month, Math.Min(relativeDate.Day, DateTime.DaysInMonth(relativeDate.Year, month)));
        }
    }
}

The Year DataSource

namespace WP7Contrib.View.Controls.DateTimePicker
{
    using System;

    public class YearDataSource : DataSource
    {
        // Methods
        /// <summary>
        /// Gets the relative to.
        /// </summary>
        /// <param name="relativeDate">The relative date.</param>
        /// <param name="delta">The delta.</param>
        /// <returns></returns>
        protected override DateTime? GetRelativeTo(DateTime relativeDate, int delta)
        {
            if ((0x641 == relativeDate.Year) || (0xbb8 == relativeDate.Year))
            {
                return null;
            }
            int year = relativeDate.Year + delta;
            return new DateTime(year, relativeDate.Month, Math.Min(relativeDate.Day, DateTime.DaysInMonth(year, relativeDate.Month)));
        }
    }
}

Now that we have redefined these datasources we want to use in our customised implementation the next part is to take a look at the guts of the Xaml that is being used to compose the Picker as we mentioned earlier on there is the LoopingSelector that can be found in the Primitives namespace. This control is rather nice and does what it says on the tin, by providing a gambling machine style interaction. If you take a look at the Xaml defined in the DateTimePicker.xaml this is essentially what we need, so again this is pretty much a copy and paste job. I created a new user control called MonthYearPicker.xaml and stuffed the Xaml in here. The only changes that we need to make in the Xaml is to change the visibility of the looping selector that you don’t want to present to the user, in my case this was the day. Now we could do some more sophisticated stuff here, like provide an attached Dependency Property to control the visibility of the selector this is on the list so watch this space, but if you can think of any others shout.

<!-- LoopingSelectors -->
        <Grid
            Grid.Row="2"
            HorizontalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Primitives:LoopingSelector
                x:Name="SecondarySelector"
                Grid.Column="0"
                Width="148"
                ItemSize="148,148"
                ItemMargin="6">
                <Primitives:LoopingSelector.ItemTemplate>
                    <DataTemplate>
                        <StackPanel
                            HorizontalAlignment="Left"
                            VerticalAlignment="Bottom"
                            Margin="6">
                            <TextBlock
                                Text="{Binding MonthNumber}"
                                FontSize="54"
                                FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                                Margin="0,-8"/>
                            <TextBlock
                                Text="{Binding MonthName}"
                                FontSize="20"
                                FontFamily="{StaticResource PhoneFontFamilyNormal}"
                                Foreground="{StaticResource PhoneSubtleBrush}"
                                Margin="0,-2"/>
                        </StackPanel>
                    </DataTemplate>
                </Primitives:LoopingSelector.ItemTemplate>
            </Primitives:LoopingSelector>
            <Primitives:LoopingSelector
                x:Name="TertiarySelector"
                Grid.Column="1"
                Width="0"
                ItemSize="148,148"
                ItemMargin="6" Foreground="{x:Null}" Visibility="Collapsed">
                <Primitives:LoopingSelector.ItemTemplate>
                    <DataTemplate>
                        <StackPanel
                            HorizontalAlignment="Left"
                            VerticalAlignment="Bottom"
                            Margin="6">
                            <TextBlock
                                Text="{Binding DayNumber}"
                                FontSize="54"
                                FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                                Margin="0,-8"/>
                            <TextBlock
                                Text="{Binding DayName}"
                                FontSize="20"
                                FontFamily="{StaticResource PhoneFontFamilyNormal}"
                                Foreground="{StaticResource PhoneSubtleBrush}"
                                Margin="0,-2"/>
                        </StackPanel>
                    </DataTemplate>
                </Primitives:LoopingSelector.ItemTemplate>
            </Primitives:LoopingSelector>
            <Primitives:LoopingSelector
                Grid.Column="2"
                x:Name="PrimarySelector"
                Width="148"
                ItemSize="148,148"
                ItemMargin="6">
                <Primitives:LoopingSelector.ItemTemplate>
                    <DataTemplate>
                        <StackPanel
                            HorizontalAlignment="Left"
                            VerticalAlignment="Bottom"
                            Margin="6">
                            <TextBlock
                                Text="{Binding YearNumber}"
                                FontSize="54"
                                FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                                Margin="0,-8"/>
                            <TextBlock
                                Text=" "
                                FontSize="20"
                                FontFamily="{StaticResource PhoneFontFamilyNormal}"
                                Foreground="{StaticResource PhoneSubtleBrush}"
                                Margin="0,-2"/>
                        </StackPanel>
                    </DataTemplate>
                </Primitives:LoopingSelector.ItemTemplate>
            </Primitives:LoopingSelector>
        </Grid>

The final piece of the puzzle is the wire up, you maybe thinking why can’t you remove the looping selectors that you don’t want ? Well I thought that same thing, however its does not seem possible as the LoopingSelector wants 3 parts to be initialised; a Primary Part / Selector; a Secondary Part / Selector; and a Tertiary Part / Selector. Therefore we have to define all of the selectors in the Xaml. Now that we have that worked out we need to bring over some of the methods from the codebehind the picker from the toolkit.

Initialisation code

public MonthYearPicker()
        {
            this.InitializeComponent();
            this.PrimarySelector.DataSource = new YearDataSource();
            this.SecondarySelector.DataSource = new MonthDataSource();
            this.TertiarySelector.DataSource = new DayDataSource();

            ((DataSource)this.PrimarySelector.DataSource).AltSelectionChanged += this.HandleAltDataSourceSelectionChanged;
            ((DataSource)this.SecondarySelector.DataSource).AltSelectionChanged += this.HandleAltDataSourceSelectionChanged;
            ((DataSource)this.TertiarySelector.DataSource).AltSelectionChanged += this.HandleAltDataSourceSelectionChanged;

            this.InitializeDateTimePickerPage(this.PrimarySelector, this.SecondarySelector, this.TertiarySelector);
        }

Composition of the parts for the picker

        /// <summary>
        /// Gets a sequence of LoopingSelector parts ordered according to culture string for date/time formatting.
        /// </summary>
        /// <returns>
        /// LoopingSelectors ordered by culture-specific priority.
        /// </returns>
        protected override IEnumerable<LoopingSelector> GetSelectorsOrderedByCulturePattern()
        {
            return GetSelectorsOrderedByCulturePattern(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern.ToUpperInvariant(), new char[] { 'Y', 'M', 'D' }, new LoopingSelector[] { this.PrimarySelector, this.SecondarySelector, this.TertiarySelector });
        }

Handles the alt data source selection changed.

        /// <summary>
        /// Handles the alt data source selection changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Controls.SelectionChangedEventArgs"/> instance containing the event data.</param>
        private void HandleAltDataSourceSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            DataSource source = (DataSource)sender;
            if (source is YearDataSource)
            {
                // Set the year part of the value
                this.PrimarySelector.DataSource.SelectedItem = source.SelectedItem;
                if (source.SelectedItem != null && this.Value.HasValue)
                {
                    this.Value = new DateTime(
                        ((DateTimeWrapper)source.SelectedItem).DateTime.Year, this.Value.Value.Month, 1);
                }
            }
            else if (source is MonthDataSource)
            {
                // Set the month part of the value
                this.SecondarySelector.DataSource.SelectedItem = source.SelectedItem;
                if (source.SelectedItem != null && this.Value.HasValue)
                {
                    // Get the last day of the month
                    this.Value = new DateTime(
                        this.Value.Value.Year, ((DateTimeWrapper)source.SelectedItem).DateTime.Month, 1);
                }
            }
            else if (source is DayDataSource)
            {
                // We don't care about the day
                this.TertiarySelector.DataSource.SelectedItem = source.SelectedItem;
            }                       
        }

And there we have it DateTime Picker control which you can customised to show the selector that you want to show under different experiences. Our main job has been to refactored some of the code in the toolkit in order to access it from outside and also the changes to the xaml, nothing too taxing. I am pretty certain that this same approach could be used on the Time Picker, I have not tried this however there does not appear to be any particular reasons why this would not be possible.

As always interested in your thoughts and feedback…

What this space for more about the WP7Contrib soon to hit the shelves at a Codeplex near you….

You can download the code from here on Codeplex and take a look at spikes where you can find this sample and others or you can get it from here

Be Sociable, Share!

2 Responses to “WP7 Contrib – Customising the DateTime Picker”

  1. I had the same requirement for a Month and Year selector only. I added a property to the DateTimePickerPageBase, ShowMonthYearOnly, that can be set when you use the control which will hide the secondary looping selector. Since its in the original code there is no need for an extra user control. Just set the property on your Datepicker and go.

    razor247
  2. @razor that is an interesting approach, when i originally built the date time picker i was using a cut of the toolkit where the source had not been released and I had to pick out the bits that needed inorder to do what i wanted. Have you subclassed the PageBase ?? or added it directly ?? Would you be interested in sending me a sample project ?? Maybe this is something that should replace my technique and be added into the WP7Contrib, ping me on twitter @RichGee

    rich

Leave a Reply