One of the coolest things in every new developer platform is the challenge to master the entire framework in a way that enables you to create high quality, greatly optimized controls that behave in exactly the same way a user would expect to. Yesterday Microsoft announced that more than 300 000 developers have already downloaded the WP7 Developer Tools Beta. Based entirely on Silverlight, phone development has never been easier and more fun than in the Windows Phone 7 environment.
With these series of blogs I want to reveal some tricky moments in developing a simple DatePicker control for WP7. The control should mimic the one described in the public Design Guidelines:
At first sight the control should be pretty easy to implement. We have a line of three buttons aligned vertically; when a button is clicked, we display a ListBox which allows a specific date component (Day, Month or Year) to be chosen. Digging deeper reveals several challenges:
The Solution:
Data Virtualization
The ListBox in WP7 uses the IList interface to communicate with its assigned data source. Only three methods from this interface are used, namely:
So, if we provide a custom IList implementation that knows how to implement these three methods we can have our data virtualized. Let’s have a look in this pretty simple virtualized data source:
public
class
VirtualizedListSource<T> : IList
{
private
int
virtualCount;
public
event
EventHandler<VirtualizedDataItemEventArgs<T>> DataItemNeeded;
public
VirtualizedListSource(
int
virtualCount)
{
this
.virtualCount = virtualCount;
}
/// <summary>
/// Gets or sets the count of virtual items.
/// </summary>
public
int
VirtualCount
{
get
{
return
this
.virtualCount;
}
set
{
this
.virtualCount = value;
}
}
/// <summary>
/// Gets the count of this data source.
/// </summary>
public
int
Count
{
get
{
return
this
.virtualCount;
}
}
/// <summary>
/// Gets the data item at the specified index.
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public
object
this
[
int
index]
{
get
{
VirtualizedDataItem<T> item =
this
.GetItemAt(index);
if
(item !=
null
)
{
item.Index = index;
return
item;
}
return
default
(T);
}
set
{
throw
new
NotImplementedException();
}
}
public
int
IndexOf(
object
value)
{
VirtualizedDataItem<T> item = value
as
VirtualizedDataItem<T>;
Debug.Assert(item !=
null
,
"VirtualizedDataItem expected."
);
if
(item !=
null
)
{
return
item.Index;
}
return
-1;
}
protected
virtual
VirtualizedDataItem<T> CreateDataItem(
int
index)
{
VirtualizedDataItem<T> item =
null
;
EventHandler<VirtualizedDataItemEventArgs<T>> eh =
this
.DataItemNeeded;
if
(eh !=
null
)
{
VirtualizedDataItemEventArgs<T> args =
new
VirtualizedDataItemEventArgs<T>(
null
);
eh(
this
, args);
item = args.Item;
}
if
(item ==
null
)
{
item =
this
.CreateDataItemInstance();
}
return
item;
}
protected
virtual
VirtualizedDataItem<T> CreateDataItemInstance()
{
return
new
VirtualizedDataItem<T>(
default
(T));
}
protected
virtual
VirtualizedDataItem<T> GetItemAt(
int
index)
{
return
this
.CreateDataItem(index);
}
}
I have intentionally skipped the rest IList implementation as all other methods are not needed and they are not the focus of this post. Whenever an item at a specified index is requested, we raise the DataItemNeeded event, and then check whether a user-defined item is supplied and if not we create a default item instance. Using the provided implementation we may easily create a data source for the “Year” ListBox:
public
class
YearsDataSource : VirtualizedListSource<DateTime>
{
public
YearsDataSource()
:
base
(9999)
{
}
protected
override
VirtualizedDataItem<DateTime> GetItemAt(
int
index)
{
//consider that lists are zero-based
index++;
return
new
VirtualizedDataItem<DateTime>(
new
DateTime(index, 1, 1));
}
}
An important thing to note here is the fact that we need an ItemTemplate specified so that the ListBox uses UI virtualization. Otherwise the entire exercise will be useless.
Infinite ListBox
Now that we have our virtualized data we can come up with a very elegant and simple implementation of the “infinite ListBox” feature. The idea is that we can specify a fairly large virtual item count and a number of logical items (for example the “Month” ListBox will have 12). We can then override the GetItemAt method of our VirtualizedListSource class, translate the desired index within the range of logical items and return the logical item at that index. Here is the sample data source:
public
class
WheelDataSource<T> : VirtualizedListSource<T>
{
private
List<VirtualizedDataItem<T>> logicalItems;
public
WheelDataSource(IEnumerable<T> items)
{
this
.logicalItems =
new
List<VirtualizedDataItem<T>>(items)
}
protected
override
VirtualizedDataItem<T> GetItemAt(
int
index)
{
return
this
.logicalItems[index %
this
.logicalItems.Count];
}
}
Having let’s say 1 million virtual items and scrolling the ListBox to the middle will give the user the perception of an endless looping ListBox – he can scroll up and down by a total of 500 000 items in each direction.
I want to dig a bit deeper here. One may wonder why I am not using my own custom panel implementation that applies UI virtualization rather than playing with the data source of the ListBox. There are some limitations in the underlying framework that implicitly force you to use only primitive controls that are already present. One limitation is the sealed classes. For example you may not create your own custom ScrollViewer implementation by simply deriving from the base one as it is marked with the “sealed” keyword. So you have two options here: a) Use the built-in one and b) Create your own one entirely from scratch, deriving from ContentControl. While creating your own implementation always has its pros, it also has its cons, especially in the context of Windows Phone. Consider things like handling manipulation events, multi-touch, animations and scrolling as well as internal interfaces that update some other internal stuff which in turn updates more internal properties and calls internal methods. Put simply creating a custom data-source that exploits the built-in ListBox seemed the easiest and simplest solution (especially as a start-up :)).
Well, that’s all for now. In the next post I will cover creating a DateListBox which has a SelectedDate property and may be used to select a Date component – Day, Month or Year. Stay tuned.
Georgi worked at Progress Telerik to build a product that added the Progress value into the augmented and virtual reality development workflow.