Telerik blogs

The next interesting part of building a Windows Phone 7 DatePicker control is the Date ListBox. That is a ListBox which should meet the following requirements:

  • Can display in a human readable manner one of the three Date components: Day, Month and Year
  • Should be “Infinite” – as discussed in my previous post.
  • Should use data virtualization (to be most efficient).

As you can see our “Infinite ListBox” project now comes in hand. The last two of the upper requirements are already implemented and all we need to do is to implement a special VirtualizedDataItem<DateTime> that provides the needed information to bind to. For a convenience I have built a parallel hierarchy of Data Sources and List Boxes:

  • Base abstract DateListBox and DateDataSource.
  • DayListBox + DayDataSource.
  • MonthListBox + MonthDataSource. 
  • YearListBox + YearDataSource

Here is how our special data item looks like:

public class DateItem : VirtualizedDataItem<DateTime>
{
    private DateComponentType componentType;
 
    public DateItem(DateTime value, DateComponentType type)
        : base(value)
    {
        this.componentType = type;
    }
 
    public DateComponentType ComponentType
    {
        get
        {
            return this.componentType;
        }
    }
 
    public string NamedDay
    {
        get
        {
            return this.Value.ToString("dddd");
        }
    }
 
    public string Day
    {
        get
        {
            return this.Value.ToString("dd");
        }
    }
 
    public string Month
    {
        get
        {
            return this.Value.ToString("MM");
        }
    }
 
    public string NamedMonth
    {
        get
        {
            return this.Value.ToString("MMMM");
        }
    }
 
    public string Year
    {
        get
        {
            return this.Value.ToString("yyyy");
        }
    }
 
    public string LeapYear
    {
        get
        {
            if (DateTime.IsLeapYear(this.Value.Year))
            {
                return "Leap year";
            }
 
            return string.Empty;
        }
    }
 
    public override int GetHashCode()
    {
        switch (this.componentType)
        {
            case DateComponentType.Day:
                return this.Value.Day.GetHashCode();
            case DateComponentType.Month:
                return this.Value.Month.GetHashCode();
            case DateComponentType.Year:
                return this.Value.Year.GetHashCode();
        }
 
        return base.GetHashCode();
    }
 
    public override bool Equals(object obj)
    {
        DateItem item = obj as DateItem;
        if (item == null || item.componentType != this.componentType)
        {
            return false;
        }
 
        switch (this.componentType)
        {
            case DateComponentType.Day:
                return this.Value.Day == item.Value.Day;
            case DateComponentType.Month:
                return this.Value.Month == item.Value.Month;
            case DateComponentType.Year:
                return this.Value.Year == item.Value.Year;
        }
 
        return false;
    }
}

It accepts two parameters - a DateTime structure and a DateComponentType that specifies the particular date component this item is associated with. It also overrides its Equals and GetHashCode methods so that items are properly selected within the ListBox.

And let’s see what actually the base DateDataSource defines:

public abstract class DateDataSource : WheelDataSource<DateTime>
{
    private DateTime value;
 
    public DateDataSource(DateTime value)
    {
        this.value = value;
        this.VirtualCount = 10000;//10 000 items is fairly enough
        this.Initialize(value);
    }
 
    public DateTime Value
    {
        get
        {
            return this.value;
        }
    }
 
    public abstract DateComponentType ComponentType
    {
        get;
    }
 
    public virtual int FindLogicalIndex(DateTime value)
    {
        for (int i = 0; i < this.LogicalItems.Count; i++)
        {
            DateItem item = this.LogicalItems[i] as DateItem;
            switch (this.ComponentType)
            {
                case DateComponentType.Day:
                    if (item.Value.Day == value.Day)
                    {
                        return i;
                    }
                    break;
                case DateComponentType.Month:
                    if (item.Value.Month == value.Month)
                    {
                        return i;
                    }
                    break;
                case DateComponentType.Year:
                    if (item.Value.Year == value.Year)
                    {
                        return i;
                    }
                    break;
            }
        }
 
        return -1;
    }
 
    protected abstract void Initialize(DateTime value);
}

As you can see the data source derives from our WheelDataSource (the one that provides the infinite impression) and adds several date related properties. It is an abstract class and delegates logical items initialization to its concrete inheritors - such as the DayDataSource:

public class DayDataSource : DateDataSource
{
    public DayDataSource(DateTime value)
        : base(value)
    {
    }
 
    public override DateComponentType ComponentType
    {
        get
        {
            return DateComponentType.Day;
        }
    }
 
    protected override void Initialize(DateTime value)
    {
        IList<VirtualizedDataItem<DateTime>> items = this.LogicalItems;
        int days = DateTime.DaysInMonth(value.Year, value.Month);
 
        for (int i = 1; i <= days; i++)
        {
            DateTime date = new DateTime(value.Year, value.Month, i);
            items.Add(new DateItem(date, DateComponentType.Day));
        }
    }
}

A number of items, equal to the number of days within the provided month, are created upon data source initialization. In a similar way are implemented MonthDataSource and YearDataSource.

Now that we have our data sources we will need a special ListBox implementation that:

  • Defines a Value property of type DateTime.
  • Binds to the appropriate data source initially and when its Value changes.
  • Is initially scrolled to the middle index so that the "Infinity" effect is achieved.

Here comes the DateListBox base class:

public abstract class DateListBox : ListBox
{
    private byte suspendBind;
    private ScrollViewer scrollViewer;
    private bool isLoaded;
 
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(DateTime), typeof(DateListBox), new PropertyMetadata(DateTime.Now, OnPropertyChanged));
 
    public DateListBox()
    {
        this.Loaded += OnLoaded;
        this.Bind();
    }
 
    public DateTime Value
    {
        get
        {
            return (DateTime)this.GetValue(ValueProperty);
        }
        set
        {
            this.SetValue(ValueProperty, value);
        }
    }
 
    public abstract DateComponentType ComponentType
    {
        get;
    }
 
    public void SuspendBind()
    {
        this.suspendBind++;
    }
 
    public void ResumeBind(bool bind)
    {
        if (this.suspendBind > 0)
        {
            this.suspendBind--;
        }
 
        if (this.suspendBind == 0 && bind)
        {
            this.Bind();
        }
    }
 
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
 
        this.scrollViewer = this.GetTemplateChild("ScrollViewer") as ScrollViewer;
    }
 
    protected abstract DateDataSource CreateDataSource();
 
    protected void Bind()
    {
        if (this.suspendBind > 0)
        {
            return;
        }
 
        this.SuspendBind();
        DateDataSource source = this.CreateDataSource();
        this.ItemsSource = source;
        this.UpdateSelectedIndex(source);
        this.ResumeBind(false);
 
        //if we are already loaded we will need to scroll to the selected index asynchronously to let the layout pass
        if (this.isLoaded)
        {
            this.Dispatcher.BeginInvoke(this.ScrollToSelectedIndex);
        }
    }
 
    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        this.isLoaded = true;
        this.ScrollToSelectedIndex();
    }
 
    private void ScrollToSelectedIndex()
    {
        if (this.scrollViewer != null)
        {
            //TODO: -2 is just for the example, to center the item on the screen more calculations are required
            this.scrollViewer.ScrollToVerticalOffset(this.SelectedIndex - 2);
        }
    }
 
    private void UpdateSelectedIndex(DateDataSource source)
    {
        int selectedIndex = source.FindLogicalIndex(this.Value);
        if (selectedIndex == -1)
        {
            return;
        }
 
        if (source.LogicalCount > 0)
        {
            int wheelsCount = source.VirtualCount / source.LogicalCount;
            if (wheelsCount > 1)
            {
                selectedIndex = selectedIndex + (wheelsCount / 2) * source.LogicalCount;
            }
        }
 
        this.SelectedIndex = selectedIndex;
    }
 
    private void OnValueChanged(DependencyPropertyChangedEventArgs e)
    {
        this.Bind();
    }
 
    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        DateListBox list = sender as DateListBox;
        if (list == null)
        {
            throw new ArgumentException("Expected DateListBox instance");
        }
 
        if (e.Property == ValueProperty)
        {
            list.OnValueChanged(e);
        }
    }
}

Two interesting things in this implementation: the way we update the selected index so that it is positioned in the middle of the ListBox and the way we scroll to it. The idea behind positioning the selected index in the middle of our virtual count is simple: we get the logical item index - e.g. second of September will be at index 1. Then we calculate how many times the logical count is contained within the virtual one and advance that logical index appropriately. In order to scroll to the already found index we will need to do a tricky thing - to get a reference to the ScrollViewer instance used to scroll ListBox's content. This is done in the OnApplyTemplate method where we search for a template child named "ScrollViewer". Just a quick note - a simple ListBox.ScrollIntoView call will not do the job because the implementation will find the first logical match of the desired item and will not advance to the middle of our virtual data source. Once we have the ScrollViewer instance we can use its ScrollToVerticalOffset method to advance to the desired index.

The DayListBox inherits DateListBox and provides implementation of the abstract methods:

public class DayListBox : DateListBox
{
    public DayListBox()
    {
        this.DefaultStyleKey = typeof(DayListBox);
    }
 
    public override DateComponentType ComponentType
    {
        get
        {
            return DateComponentType.Day;
        }
    }
 
    protected override DateDataSource CreateDataSource()
    {
        return new DayDataSource(this.Value);
    }
}
 

A quick overview of the XAML that lies behind:

<Style TargetType="ctrl:DayListBox">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="ItemContainerStyle" Value="{StaticResource DateListBoxItem}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBox">
                <ScrollViewer x:Name="ScrollViewer" Foreground="{TemplateBinding Foreground}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
                    <ItemsPresenter/>
                </ScrollViewer>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemTemplate">
        <Setter.Value>
            <DataTemplate>
                <StackPanel Style="{StaticResource DateContent}">
                    <TextBlock Text="{Binding Path=Day}" FontSize="{StaticResource PhoneFontSizeExtraLarge}" FontWeight="Bold"/>
                    <TextBlock Text="{Binding Path=NamedDay}" FontSize="{StaticResource PhoneFontSizeSmall}" />
                </StackPanel>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

Nothing special here - as an ItemTemplate we specify a stack panel with two text blocks that bind to the desired properties of our DateItem. The MonthListBox and YearListBox styles look pretty much the same except that different DateItem properties are bound.

And here is what we have achieved far:

 

In the next post I will explain how manipulation events are handled so that the item at the selected index is always vertically centered.

Attached is the sample project used to create this post. Enjoy :)

BlogTest.zip


Georgi Atanasov 164x164
About the Author

Georgi Atanasov

Georgi worked at Progress Telerik to build a product that added the Progress value into the augmented and virtual reality development workflow.

 

Comments

Comments are disabled in preview mode.