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:
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:
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:
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 :)
Georgi worked at Progress Telerik to build a product that added the Progress value into the augmented and virtual reality development workflow.