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.