0

I'm working on an application in which I have a lot of sliders with labels above them like so:

Labeled Slider

In the interest of re-usability, I'd like to create a custom LabeledSlider control with extra properties for the left and right label strings. A simple UserControl seemed like a good fit at first, until I realized I would have to re-implement all of the Slider properties and methods I planned to use (there are a lot of them).

So inheriting from Slider seems like the better choice, but it looks like I have to create a custom style to do so. Is there a way to derive from Slider and add these labels while preserving the existing style?

For what it's worth, I'm using MahApps.Metro.

Neil
  • 249
  • 3
  • 13
  • Instead of creating a custom control, you could create a custom ControlTemplate for the slider. For the left/right labels, you could either create two attached properties for the Slider type which will provide the left/right label content. This way, the slider remains the bog-standard slider class, but you can furnish it with custom view + properties. Or instead of custom attached properties, you could simply subclass the slider control and add the properties to the subclassed Slider class. –  Jun 21 '17 at 15:59
  • The attached property approach is interesting, but I think that subclassing `Slider` is the better approach here. How would I go about writing a `ControlTemplate` that doesn't require re-style the entire slider? – Neil Jun 21 '17 at 19:47
  • Why do you think that you would need to restyle the slider just because you would make your own control template? Usually, making your own control templates does not force you to restyle anything, unless you intentionally want to restyle something. I guess i am missing a piece of information to fully understand your situation... –  Jun 21 '17 at 19:49
  • Doesn't writing a custom `ControlTemplate` replace the entire visual tree of the control? I which case I suppose I could use a `Slider` within the `ControlTemplate` for my custom `Slider` and somehow bind all of it's properties up to the "outer" `Slider`. My head hurts... – Neil Jun 21 '17 at 20:06
  • Yes. So, i guess you are not talking about styles but rather about template-binding the properties of the slider within the template to the properties of the (outer) slider control which uses the control template. Is my guess correct? –  Jun 21 '17 at 20:08
  • That's correct. – Neil Jun 21 '17 at 20:09
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/147307/discussion-between-elgonzo-and-neil-thiessen). –  Jun 21 '17 at 20:09

1 Answers1

4

If you want to avoid all the plumbing that comes with realizing your custom slider with a UserControl, you can instead use a Slider control directly with a custom control template.


The control template

The custom control template would define all the UI elements necessary to present your custom slider; like the two labels, an "inner" Slider control (the actual slider shown in the UI), and any other desired/required UI elements.

An (incomplete) illustration of how a simple form of such a control template could look like:

    <Slider
        TickPlacement="Both"
        TickFrequency="0.5"
        BorderBrush="Red"
        BorderThickness="3" >
        <Slider.Template>

            <ControlTemplate TargetType="Slider">
                <Border
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>

                        <TextBlock Text="Left label" Grid.Column="0" Grid.Row="0" />
                        <TextBlock Text="Right label" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" />
                        <Slider Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1"
                            BorderThickness="0"
                            Value="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                            TickFrequency="{TemplateBinding TickFrequency}
                            TickPlacement="{TemplateBinding TickPlacement}"
                            ...
                        />
                    </Grid>
                </Border>
            </ControlTemplate>

        </Slider.Template>
    </Slider>

Note that all the properties of the "outer" slider that should be passed through to the "inner" slider should have corresponding TemplateBindings for the "inner" slider (as illustrated with the Value, TickFrequency and TickPlacement properties).

Pay attention to the template binding of the Value property. For any template binding that needs to be two-way, the shorthand form {TemplateName SourceProperty} will not do as it is one-way. For two-way template bindings, the binding should be declared like {Binding SourceProperty, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}.

Some properties of the "outer" slider you might not wish to pass through the "inner" slider. Perhaps you would like the border (a slider control supports borders) to surround the slider together with the text labels. For this case, my example control template above features a Border element surrounding the "inner" slider and the text labels. Note that border-related properties from the "outer" slider control using the control template are being template-bound to this Border element and not the "inner" slider.

However, there would still be an issue with how the border is handled. If you (or someone else) at some time defines a default style for sliders that incorporates a border, then the respective border parameters of this default style would also apply to the "inner" slider -- something that would be undesired. To prevent this from happening, my example control template explicitly sets the value of BorderThickness for the "inner" slider.

Do so in a similar manner for any other properties of the UI elements in the control template you do not wish to be affected by their respective default styles.


Attached properties for the label text

If you wish to be able to change the text of the labels through bindings, you need to introduce some properties for them. One way of achieving this is to realize them as attached properties. Their implementation can be rather simple:

public static class SliderExtensions
{
    public static string GetLeftLabel(DependencyObject obj)
    {
        return (string)obj.GetValue(LeftLabelProperty);
    }

    public static void SetLeftLabel(DependencyObject obj, string value)
    {
        obj.SetValue(LeftLabelProperty, value);
    }

    public static readonly DependencyProperty LeftLabelProperty = DependencyProperty.RegisterAttached(
        "LeftLabel",
        typeof(string),
        typeof(SliderExtensions)
    );


    public static string GetRightLabel(DependencyObject obj)
    {
        return (string)obj.GetValue(RightLabelProperty);
    }

    public static void SetRightLabel(DependencyObject obj, string value)
    {
        obj.SetValue(RightLabelProperty, value);
    }

    public static readonly DependencyProperty RightLabelProperty = DependencyProperty.RegisterAttached(
        "RightLabel",
        typeof(string),
        typeof(SliderExtensions)
    );
}

The code above provides two attached properties (of type string) called "SliderExtensions.LeftLabel" and "SliderExtensions.RightLabel". You could then employ them as follows:

    <Slider
        local:SliderExtensions.LeftLabel="Port"
        local:SliderExtensions.RightLabel="Starboard"
        TickPlacement="Both"
        TickFrequency="0.5"
        BorderBrush="Red"
        BorderThickness="3" >
        <Slider.Template>
            <ControlTemplate TargetType="Slider">
                ...
                <TextBlock Text="{TemplateBinding local:SliderExtensions.LeftLabel}" Grid.Column="0" Grid.Row="0" />
                <TextBlock Text="{TemplateBinding local:SliderExtensions.RightLabel}" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" />
                 ...
             ...


A custom slider class with dependency properties for the label text

If you don't want to use attached properties, you could also derive your own custom slider control from the standard Slider class and implement LeftLabel and RightLabel as dependency properties of this custom slider class.

public class MyCustomSlider : Slider
{
    public string LeftLabel
    {
        get { return (string)GetValue(LeftLabelProperty); }
        set { SetValue(LeftLabelProperty, value); }
    }

    public static readonly DependencyProperty LeftLabelProperty = DependencyProperty.Register(
        nameof(LeftLabel),
        typeof(string),
        typeof(MyCustomSlider)
    );


    public string RightLabel
    {
        get { return (string)GetValue(RightLabelProperty); }
        set { SetValue(RightLabelProperty, value); }
    }

    public static readonly DependencyProperty RightLabelProperty = DependencyProperty.Register(
        nameof(RightLabel),
        typeof(string),
        typeof(MyCustomSlider)
    );
}

This approach has the added benefit that you are able to define a default style specifically for your MyCustomSlider type. See this answer for more information related to that topic.

If you prefer to do this, then don't forget to adjust the target type of the control template:

    <local:MyCustomSlider
        LeftLabel="Port"
        RightLabel="Starboard"
        TickPlacement="Both"
        TickFrequency="0.5"
        BorderBrush="Red"
        BorderThickness="3" >
        <local:MyCustomSlider.Template>
            <ControlTemplate TargetType="TargetType="local:MyCustomSlider"">
                ...
                <TextBlock Text="{TemplateBinding LeftLabel}" Grid.Column="0" Grid.Row="0" />
                <TextBlock Text="{TemplateBinding RightLabel}" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" />
                 ...
             ...

You are not forced to use string properties and TextBlocks for the labels. You could use ContentPresenter instead of the TextBlock and make the LeftLabel and RightLabel properties of type object. This way you can still use text labels but - if desired - you could also use any other content for them (such as images, for example).


Side note: My answer is based on the standard WPF slider control. If you are using a slider control provided by MahApps.Metro (which i do know only very little about), certain details -- such as the naming, type, presence or absence of properties -- might perhaps differ from what my answer shows.

  • 1
    Thanks for the awesome answer! One thing I noticed is I had to bind the `Value` property of the inner `Slider` using `{Binding Value, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}` since [TemplateBindings are apparently one-way by default](https://stackoverflow.com/a/5913305/4033515). – Neil Jun 21 '17 at 21:49