Binding a Slider to an Enumeration in UWP

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:

EnumSlider

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:

EnumSlider_DesignModeFail

Here’s the same IDE after we added the null-check:

EnumSlider_DesignMode

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’:

EnumSliderDisplay

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:

xBindIssue

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:

EnumSlider_Phone1 EnumSlider_Phone2

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!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s