Very often the values inside an enumeration type represent a sequence or a ranking, like small-medium-large or good-better-best. In a UI it would be appropriate to represent such a field through a slider control. This article explains how to build such a slider in XAML and C# and how to use it in a MVVM Windows 8 Store app. The slider is implemented as a lightweight user control to allow maximum templatability. The template just comes with a TextBlock that displays the value, and a Slider to change that value. Here’s the template:
<Style TargetType="local:EnumerationSlider">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:EnumerationSlider">
<StackPanel>
<TextBlock Name="PART_TextBlock"
Text="{TemplateBinding Value}"
FontSize="24" />
<Slider Name="PART_Slider"
SnapsTo="StepValues"
StepFrequency="1"
IsThumbToolTipEnabled="False" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The control has two properties, implemented as Dependency Property. Enumeration is the string that contains the fully qualified name of the Enum to which you want to bind, and Value is the property that holds the current enumeration value as a string.
When the Enumeration property is assigned, we populate an internal collection with the enumeration values. The Slider's steps represent the index in that collection, so we reset its maximum to the number of items in the enumeration (minus one). Here’s that process:
private List<string> names;
private static void OnEnumerationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EnumerationSlider es = d as EnumerationSlider;
if (es != null)
{
Type t = Type.GetType(es.Enumeration);
es.names = Enum.GetNames(t).ToList();
var slider = es.GetTemplateChild(SliderPartName) as Slider;
if (slider != null)
{
slider.Value = 0;
slider.Maximum = es.names.Count() - 1;
}
}
}
When the viewmodel triggers a value change –e.g. when initializing a binding- we calculate the numeric value for the Slider by looking it up in the private names collection:
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EnumerationSlider es = d as EnumerationSlider;
if (es != null)
{
var slider = es.GetTemplateChild(SliderPartName) as Slider;
if (slider != null)
{
slider.Value = es.names.IndexOf(es.Value.ToString());
}
}
}
The other way round, when the Slider is pushed by the user, we continuously update the control’s Value property:
protected override void OnApplyTemplate()
{
var slider = this.GetTemplateChild(SliderPartName) as Slider;
if (slider != null)
{
slider.ValueChanged += Slider_ValueChanged;
}
// ...
}
private void Slider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
this.Value = this.names[(int)(sender as Slider).Value];
}
The control is ready now, it can be bound to any enumeration. So let’s first define some enumerations. They represent the size and temperature of Starbucks coffees:
namespace Enumerations
{
public enum Sizes
{
Demi,
@Short,
Tall,
Grande,
Venti,
Trenta
}
public enum Temperatures
{
Cold,
LukeWarm,
Warm,
Hot,
Boiling
}
}
Here’s a screenshot of the demo app. The two sliders on it are bound to two different enumerations:
The EnumerationSlider exposes its Value as a string, not as an instance of the linked Enumeration type. So it cannot be bound to a viewmodel’s Enum-type property through a TwoWay Binding. When you move the Slider, an "Error: Cannot save value from target back to source." will be produced. You can solve that problem with a ValueConverter, but then you would need to write one for each enumeration type, and use it in every binding. You could also create a wrapper class for each enumeration that hosts the string-to-value conversion. If you search for ‘BindableEnum’ you’ll find examples of this.
I personally prefer to decorate the viewmodel with a wrapper property of type string that passes the value to the 'real' property, like this:
public class MainPageViewModel : BindableBase
{
private Sizes coffeeSize = Sizes.Grande;
/// <summary>
/// Coffee Size property as it comes directly from the Mvvm Model.
/// </summary>
public Sizes CoffeeSize
{
get { return coffeeSize; }
set
{
this.SetProperty(ref this.coffeeSize, value);
this.OnPropertyChanged("CoffeeString");
}
}
/// <summary>
/// Wrapper around the CoffeeSize property to make it bindable.
/// </summary>
public string CoffeeString
{
get { return this.coffeeSize.ToString(); }
set { this.CoffeeSize = (Sizes)System.Enum.Parse(typeof(Sizes), value); }
}
// ...
}
That code may look cumbersome, but at least it’s in the correct place. The extra property makes the model easily bindable to the view. That’s exactly what a viewmodel is supposed to do. Here’s how to bind viewmodel to the control:
<controls:EnumerationSlider Enumeration="Enumerations.Sizes"
Value="{Binding CoffeeString, Mode=TwoWay}" />
<controls:EnumerationSlider Enumeration="Enumerations.Temperatures"
Value="{Binding TemperatureString, Mode=TwoWay}" />
That's it! We now have a Slider that's two-way-bindable to any enumeration property of a viewmodel.
Here’s the code: U2UConsult.WinRT.EnumerationSlider.zip (493.88 kb)
Enjoy!
Diederik