Abstracts
Windows Phone controls toolkit has a great control for displaying date/time (DatePicker and TimePicker). Fortunately, it is quite easy to build based on them a control that allows selecting digits. For example, I am working on a solution to allow users select metric parameters – height, width, cost etc. As a result, I implemented own control based on LoopingSelector and ILoopingSelectorDataSource.
Implementation
DatePicker and TimePicker controls are a set of primitive controls – box for displaying data, additional PhonePage and looping selectors themselves. If you activate a Picker, you will see the next screen:
This screen contains three looping selectors – for month, day and year. The control that is used for these elements is LoopingSelector. It is not sealed and allows inheritance.
It is needed to define several additional fields for LoopingSelector control to allow developers set max/min bound, step, default value etc.
Here is the list of dependency properties I define in the control:
/// <summary>
/// The MaxValue DependencyProperty.
/// </summary>
public static readonly DependencyProperty MaxValueProperty =
DependencyProperty.Register("MaxValue", typeof(int), typeof(DigitLoopingSelector), new PropertyMetadata(100, ValueChanged));
/// <summary>
/// The MinValue DependencyProperty.
/// </summary>
public static readonly DependencyProperty MinValueProperty =
DependencyProperty.Register("MinValue", typeof(int), typeof(DigitLoopingSelector), new PropertyMetadata(0, ValueChanged));
/// <summary>
/// The Step DependencyProperty.
/// </summary>
public static readonly DependencyProperty StepProperty =
DependencyProperty.Register("Step", typeof(int), typeof(DigitLoopingSelector), new PropertyMetadata(1, ValueChanged));
/// <summary>
/// The Step DependencyProperty.
/// </summary>
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(int), typeof(DigitLoopingSelector), new PropertyMetadata(0, ValueChanged));
/// <summary>
/// The Step DependencyProperty.
/// </summary>
public static readonly DependencyProperty DefaultValueProperty =
DependencyProperty.Register("DefaultValue", typeof(int), typeof(DigitLoopingSelector), new PropertyMetadata(0, ValueChanged));
/// <summary>
/// The Step DependencyProperty.
/// </summary>
public static readonly DependencyProperty StringFormatProperty =
DependencyProperty.Register("StringFormat", typeof(string), typeof(DigitLoopingSelector), new PropertyMetadata(string.Empty, ValueChanged));
private static void ValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
DigitLoopingSelector picker = (DigitLoopingSelector)obj;
picker.DataSource = new DigitDataSource(picker.MinValue, picker.MaxValue, picker.Step, picker.DefaultValue, picker.StringFormat);
}
Additionally, I define StringFormat and SelectedItem properties. First one allows defining format for a displaying digits. For example, If I display two selectors for selecting meters/centimeters (feet/inches etc) I would prefer to add leading zeros for centimeters/inches when they are lower then ten. The second property allow quickly selecting a new current value for the control.
But that control cannot work without datasource. it provide data that will be displayed, apply formatting etc. I am going to implement ILoopingSelectorDataSource interface. It contains three methods and one event:
/// <summary>
/// Defines how the LoopingSelector communicates with data source.
/// </summary>
public interface ILoopingSelectorDataSource
{
/// <summary>
/// Get the next datum, relative to an existing datum.
/// </summary>
/// <param name="relativeTo">The datum the return value will be relative to.</param>
/// <returns>The next datum.</returns>
object GetNext(object relativeTo);
/// <summary>
/// Get the previous datum, relative to an existing datum.
/// </summary>
/// <param name="relativeTo">The datum the return value will be relative to.</param>
/// <returns>The previous datum.</returns>
object GetPrevious(object relativeTo);
/// <summary>
/// The selected item. Should never be null.
/// </summary>
object SelectedItem { get; set; }
/// <summary>
/// Raised when the selection changes.
/// </summary>
event EventHandler<SelectionChangedEventArgs> SelectionChanged;
}
GetNext and GetPrevious methods should return values that are after and before the current value accordingly. SelectedItem allows getting and setting a current value.SelectionChanged event should be fired when SelectedItem is changed.
Here is my implementation:
public class DigitDataSource : ILoopingSelectorDataSource
{
public DigitDataSource(int minValue, int maxValue, int step, int defaultValue, string stringFormat)
{
MinValue = minValue;
MaxValue = maxValue;
Step = step;
SelectedItem = defaultValue;
StringFormat = stringFormat;
}
public int MinValue
{
get;
set;
}
public int MaxValue
{
get;
set;
}
public int Step
{
get;
set;
}
private string ApplyFormat(int digit)
{
return digit.ToString(StringFormat);
}
public string StringFormat
{
get;
private set;
}
#region ILoopingSelectorDataSource Members
public object GetNext(object relativeTo)
{
return Convert.ToInt32(relativeTo) + Step > MaxValue ? ApplyFormat(MinValue) : ApplyFormat(Convert.ToInt32(relativeTo) + Step);
}
public object GetPrevious(object relativeTo)
{
return Convert.ToInt32(relativeTo) - Step < MinValue ? ApplyFormat(MaxValue) : ApplyFormat(Convert.ToInt32(relativeTo) - Step);
}
public int selectedItem;
public object SelectedItem
{
get
{
return ApplyFormat(selectedItem);
}
set
{
int newValue = Convert.ToInt32(value);
if (selectedItem != newValue)
{
int previousSelectedItem = selectedItem;
selectedItem = newValue;
EventHandler<SelectionChangedEventArgs> handler = SelectionChanged;
if (handler != null)
handler(this, new SelectionChangedEventArgs(new object[] { ApplyFormat(previousSelectedItem) }, new object[] { ApplyFormat(selectedItem) }));
}
}
}
public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
#endregion
}
GetNext and GetPrevious methods returns data in any case. For example, if the next value is more than maximum, then the method should return minimal value and vice versa. Both methods return formatted string.
Of course, if you need more complex algorithm for displaying digits, you can implement your own ILoopingSelectorDataSource and bind to a looping control.
You can use following code to allows users selecting width of some construction:
<Grid x:Name="WidthPanel" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.1*"/>
<ColumnDefinition Width="0.35*"/>
<ColumnDefinition Width="0.15*"/>
</Grid.ColumnDefinitions>
<my:DigitLoopingSelector
Grid.Column="0"
Name="WidthMeterSelector"
Size="108,108"
Margin="6,3" HorizontalAlignment="Right" Margin="0" Width="108" IsExpanded="True"
MaxValue="15" MinValue="1" DefaultValue="3" SelectedItem="{Binding WidthMeter}" >
<my:DigitLoopingSelector.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding BindsDirectlyToSource=True}"
FontSize="54" TextAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource PhoneFontFamilySemiBold}"/>
</DataTemplate>
</my:DigitLoopingSelector.ItemTemplate>
</my:DigitLoopingSelector>
<TextBlock Grid.Column="1" Margin="0,0,0,0" TextWrapping="Wrap" Text="m" FontSize="48" VerticalAlignment="Center"/>
<my:DigitLoopingSelector
Grid.Column="2"
Name="WidthCentimeterSelector"
Size="108,108"
Margin="6,3"
MaxValue="99" StringFormat="D2"
MinValue="0" HorizontalAlignment="Right" Margin="48,0,0,0" Width="108" IsExpanded="True"
>
<my:DigitLoopingSelector.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding BindsDirectlyToSource=True}"
FontSize="54" TextAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource PhoneFontFamilySemiBold}"/>
</DataTemplate>
</my:DigitLoopingSelector.ItemTemplate>
</my:DigitLoopingSelector>
<TextBlock Grid.Column="3" Margin="0" TextWrapping="Wrap" Text="cm" FontSize="48" VerticalAlignment="Center"/>
</Grid>
On the following figure you can see how it could look like in your application:
Of course, you can put the controls in a smaller panel, change font size for digits, size of boxes etc. according to your needs.
Sources
You can grab sources here.
Conclusion
The DigitLoopingSelector control is very useful and easy to use – you need just use my superstructure for the control and datasource or implement your one for tricky scenarios.