Very often an Enumeration type represents a sequence or a ranking, like Offline-Connecting-Online or Bad-Good-Better-Best. In a user interface it would make sense to bind a property of such an Enum to a Slider control and let it more or less behave like a rating control.
In this article we’ll build a templatable custom control that
- looks and behaves like a Slider,
- can be two-way bound to most Enum types,
- supports {x:Bind} and {Binding} declarative binding as well as programmatic binding,
- provides a tooltip while sliding,
- respects the Display data annotation attribute to support custom text, and
- does not crash the XAML designer (!).
This is a screenshot from the sample app. It shows three instances of the EnumSlider control. Each of the sliders is bound in a different way to the same Importance property in the viewmodel:
Style
It is possible to derive from the Slider base class, but we went for a templatable control. Here’s the default template, it contains just a Slider:
<Style TargetType="local:EnumSlider"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:EnumSlider"> <Slider Name="PART_Slider" SnapsTo="StepValues" StepFrequency="1" /> </ControlTemplate> </Setter.Value> </Setter> </Style>
Class Definition
Properties and Initialization
The class definition holds a private field to store the Enum type, and a Value dependency property of type Object:
// The Enum Type to which we are bound. private Type _enum; // Value Dependency Property. public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof(object), typeof(EnumSlider), new PropertyMetadata(null, OnValueChanged)); /// <summary> /// Gets or sets the Value. /// </summary> public object Value { get { return GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }
When the template is applied to the control, we initialize the Slider:
/// <summary> /// Called when the (default or custom) style template is applied. /// </summary> protected override void OnApplyTemplate() { if (_enum == null) { // Keep the XAML Designer happy. return; } InitializeSlider(); }
During that initialization,
- we set the slider’s range to the number of constants in the enumerator list,
- we register an event handler to ValueChanged,
- we update its Value by casting to integer, and
- we assign a ThumbToolTipValueConverter so that the tooltip –while sliding- shows text instead of the numerical value:
/// <summary> /// Configures the internal Slider. /// </summary> private void InitializeSlider() { var slider = this.GetTemplateChild(SliderPartName) as Slider; if (slider != null) { slider.ValueChanged += Slider_ValueChanged; slider.Maximum = Enum.GetNames(this._enum).Count() - 1; slider.ThumbToolTipValueConverter = new DoubleToEnumConverter(_enum); slider.Value = (int)this.Value; } }
The range of the slider now goes from zero to the number of items in the enumeration (minus one), and we’re using the default Enum-to-int cast to map the values. This technique does NOT work when the enumeration does not use an integral type as its underlying type. It also does NOT work for enumerations that override their sequence, like this:
enum Importance { None = -1, // Does not work. Trivial, Moderate, Important = 10, // Does not work. Critical };
In this version of the EnumSlider, we’ll NOT work around these issues. We assume that an enumeration without linear sequence numbers is probably not a good candidate to be bound to a linear slider. We like to keep this control lightweight, and hence only focus on the ‘regular’ enumerations.
If you *do* want a version of a slider control that deals with custom sequence numbers and underlying data types, please check this older version that we wrote a couple of years ago. Its mapping logic is built upon an internal dictionary with the enumeration’s text values.
Keeping the XAML Designer happy
The OnApplyTemplate is not only called at runtime. It may also be called at design time, by the XAML Designer. At that moment the Enum type is not yet known, so we should not try to configure the slider then. That’s why there is a null-check on _enum in the start of OnApplyTemplate. If we would trigger the slider initialization there, it would fail. The code would compile and run, but the IDE will not be able to preview any page that hosts the control. You’ll end up with an empty designer, and some squiggly lines in the XAML:
Here’s the same IDE after we added the null-check:
If you want developers to use your control in their apps, make sure it does not crash the XAML designer.
Runtime behavior
When the Value property of our control changes, all we need to do is updating the internal slider. When app starts, the very first update(s) may occur before the control is ready (i.e. before OnApplyTemplate is called). So the OnValueChanged event handler starts with a check to see whether or not the slider still needs to be initialized. This is also the place where we get our hands on the real Enum type to which the control is bound. At the end of the method, we update the slider’s value with that same cast from Enum to Integer:
/// <summary> /// Called when the Value changed, e.g. through data binding. /// </summary> private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var _this = d as EnumSlider; if (_this != null) { // Initialize the Enum Type. if (e.OldValue == null) { if (e.NewValue is Enum) { _this._enum = e.NewValue.GetType(); _this.InitializeSlider(); return; // Slider got its value. } } var slider = _this.GetTemplateChild(SliderPartName) as Slider; if (slider != null) { slider.Value = (int)_this.Value; } } }
Converters, converters, converters
Dealing with data annotations
In the tooltip that is shown when the user manipulates the slider, we’re going to show the text value that corresponds to the current slider value (a Double). Unfortunately, enumerator constants cannot contain special characters or spaces, and they’re also not localizable. It’s however possible to decorate them with a Display data annotation attribute, like this:
enum Importance { None, Trivial, Moderate, Important, [Display(Name = "O M G")] Critical };
The lookup of the corresponding text value is done in the converter instance that we assigned in the slider’s initialization. The string representation of the enumerator –also its name in the type definition- is found with a call to Enum.ToObject(). Then we apply some reflection –with GetRuntimeFields and GetCustomAttribute– to get to the Display attribute’s value:
/// <summary> /// Converts the value of the internal slider into text. /// </summary> /// <remarks>Internal use only.</remarks> internal class DoubleToEnumConverter : IValueConverter { private Type _enum; public DoubleToEnumConverter(Type type) { _enum = type; } public object Convert(object value, Type targetType, object parameter, string language) { var _name = Enum.ToObject(_enum, (int)(double)value); // Look for a 'Display' attribute. var _member = _enum .GetRuntimeFields() .FirstOrDefault(x => x.Name == _name.ToString()); if (_member == null) { return _name; } var _attr = (DisplayAttribute)_member .GetCustomAttribute(typeof(DisplayAttribute)); if (_attr == null) { return _name; } return _attr.Name; } public object ConvertBack(object value, Type targetType, object parameter, string language) { return value; // Never called } }
[Remember to run your app from time to time in release mode, especially when using reflection!]
Here’s the result. The tooltip of the middle slider shows ‘O M G’ instead of ‘Critical’:
Alternatively you can use the Display value for looking up a localized value from a resource. For an example of this, check this article by Marco Minerva.
Dealing with {x:Bind}
Regular bindings are evaluated at runtime, but when you declaratively bind with the newer {x:Bind} construction, then some of the binding code is generated at compile-time. For a two-way binding, the compiler will verify the compatibility between the viewmodel’s property (in our case: the enumeration) and the control’s property (in our case: the Value property of type Object). The compiler will not be happy if we define the binding like this:
<controls:EnumSlider Value="{ x:Bind ViewModel.Importance, Mode=TwoWay />
Here’s the complaint:
To reassure the compiler, we are forced to plug in a converter. Here’s the full code of that converter:
/// <summary> /// Facilitates two-way binding to an Enum. /// </summary> public class EnumConverter : IValueConverter { private Type _enum; public object Convert( object value, Type targetType, object parameter, string language) { _enum = value.GetType(); return value; } public object ConvertBack( object value, Type targetType, object parameter, string language) { if (_enum == null) { return null; } return Enum.ToObject(_enum, (int)value); } }
Here’s the working version of the {x:Bind} declaration in XAML:
<controls:EnumSlider Value="{ x:Bind ViewModel.Importance, Mode=TwoWay, Converter={StaticResource EnumConverter}}" />
For a regular –old school- binding, the converter is not necessary:
<controls:EnumSlider DataContext="{x:Bind ViewModel}" Value="{Binding Importance, Mode=TwoWay}" />
OK, we covered the two declarative binding techniques. Let’s now take a look at programmatic binding. Here’s the XAML:
<controls:EnumSlider x:Name="CodeBehindSlider" />
And here’s the C# bit:
this.CodeBehindSlider.BindTo(ViewModel, "Importance");
For convenience, the EnumSlider has a helper method that facilitates setting up a two-way binding to its Value:
/// <summary> /// Sets up a two-way data binding. /// </summary> public bool BindTo(object viewModel, string path) { try { Binding b = new Binding(); b.Source = viewModel; b.Path = new PropertyPath(path); b.Mode = BindingMode.TwoWay; // b.Converter = new EnumConverter(); this.SetBinding(EnumSlider.ValueProperty, b); } catch (Exception) { return false; } return true; }
[It turns out that the converter is optional, so this helper method does not actually add much value…]
Wait, there’s more: … another converter
The logic in the internal converter that looks up the value for the Display attribute, is so useful that we decided to expose it. The control’s project has a public EnumDisplayConverter that converts an enumeration instance to its display value. The code is roughly the same as in the DoubleToEnumConverter, and it can be used to bind a text to the enum property. This is the XAML for the text block in the upper right corner of the page:
<TextBlock Text="{ x:Bind ViewModel.Importance, Mode=OneWay, Converter={StaticResource EnumDisplayConverter}}" />
Alternatively, we could have exposed the logic as an extension method of Enum, but then it would be less discoverable.
It’s Universal
We haven’t tested the project on a Raspberry Pi yet, but here’s how it looks like on the phone:
![]() |
![]() |
Takeaways
Here are some takeaways for custom control builders:
- Write defensive code in OnApplyTemplate and OnValueChanged event handlers. You never know in which order they are called.
- Make sure to not crash the XAML Designer.
- Test your controls against all types of data binding.
- Test your controls in release mode from time to time.
- If your control contains logic that could be useful to the client, expose that logic.
Source
The sample app and the control live here on GitHub. The EnumSlider control has its own project, for easy reuse.
Enjoy!