0

My question is based on one of the answer to this post: Where is the WPF Numeric UpDown control? answered by Mr. Squirrel.Downy. What i would like to accomplish is a numeric updown control that increases/decreases with a larger amount when the buttons are pressed for a longer time, otherwise the increase/decrease is the normal amount. Also when the max/min is reached the buttons should disable. I have a style based on a Slider which contains 2 buttons of type HoldButton (up/down, derived from RepeatButton) and a readonly TextBlock for the Value. In the HoldButton i have 2 dependency properties for an ICommand. These are ClickAndHoldCommand and ClickCommand which are executed from either the OnPreviewMouseLeftButtonDown() or OnPreviewMouseLeftButtonUp() depending on the length of the mouse button press. In the xaml these are bound to Slider.IncreaseLarge and Slider.IncreaseSmall respectively. How can i disable the up button when the max is reached and disable the down button when the min is reached? The difficulty is that when i for example disable the slider, the up mouse event does not work anymore...

<Style TargetType="{x:Type Slider}" x:Key="NumericUpDown">
    <Style.Resources>
        <Style x:Key="RepeatButtonStyle" TargetType="{x:Type RepeatButton}">
            <Setter Property="Focusable" Value="false" />
            <Setter Property="IsTabStop" Value="false" />
            <Setter Property="Padding" Value="0" />
            <Setter Property="Width" Value="20" />
        </Style>
    </Style.Resources>
    <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false" />
    <Setter Property="SmallChange" Value="1" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Slider}">
                <Grid>
                    <Rectangle RadiusX="10" RadiusY="10" Stroke="{StaticResource SolidBrushLightGrey}" Fill="Black" StrokeThickness="1" />
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <TextBlock Grid.Row="0" x:Name="ControlName" Style="{StaticResource LabelStyle}" Margin="0,5,0,0" />
                        <TextBlock Grid.Row="1" x:Name="ControlUnits" Style="{StaticResource LabelStyle}" Margin="0,5,0,0" />
                        <usercontrols:HoldButton Grid.Row="2" Delay="250" Interval="375" 
                                                 EnableClickHold="True" 
                                                 ClickAndHoldCommand="{x:Static Slider.IncreaseLarge}" 
                                                 ClickCommand="{x:Static Slider.IncreaseSmall}"
                                                 MaxWidth="60" Height="60" Width="60" Style="{StaticResource ButtonStyleGeneral}" Content="+">
                        </usercontrols:HoldButton>
                        
                        <TextBlock Grid.Row="3" x:Name="Temperature" Style="{StaticResource LabelStyle}" Margin="0,5,0,0" FontSize="30" Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Value, StringFormat=N1}" />

                        <usercontrols:HoldButton Grid.Row="4" Delay="250" Interval="375" 
                                                 EnableClickHold="True" 
                                                 ClickAndHoldCommand="{x:Static Slider.DecreaseLarge}" 
                                                 ClickCommand="{x:Static Slider.DecreaseSmall}" 
                                                 MaxWidth="60" Height="60" Width="60" Style="{StaticResource ButtonStyleGeneral}" Content="-">
                        </usercontrols:HoldButton>
                        
                        <Border x:Name="TrackBackground" Visibility="Collapsed">
                            <Rectangle x:Name="PART_SelectionRange" Visibility="Collapsed" />
                        </Border>
                        <Thumb x:Name="Thumb" Visibility="Collapsed" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
public partial class HoldButton : RepeatButton
{
    private bool buttonIsHeldPressed;

    public HoldButton()
    {
        InitializeComponent();
        buttonIsHeldPressed = false;

        this.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
        
        // RepeatButton fires click event repeatedly while button is being pressed!
        this.Click += HoldButton_Click;
    }

    private void HoldButton_Click(object sender, RoutedEventArgs e)
    {
        Trace.WriteLine("HoldButton_Click()");

        if (EnableClickHold)
        {
            if (numberButtonRepeats > 2)
            {
                ClickAndHoldCommand.Execute(this.CommandParameter);
                e.Handled = true;
                buttonIsHeldPressed = true;
            }

            numberButtonRepeats++;
        }
    }

    public bool EnableClickHold
    {
        get { return (bool)GetValue(EnableClickHoldProperty); }
        set { SetValue(EnableClickHoldProperty, value); }
    }

    public ICommand ClickAndHoldCommand
    {
        get { return (ICommand)GetValue(ClickAndHoldCommandProperty); }
        set { SetValue(ClickAndHoldCommandProperty, value); }
    }

    public ICommand ClickCommand
    {
        get { return (ICommand)GetValue(ClickCommandProperty); }
        set { SetValue(ClickCommandProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ClickAndHoldCommand.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ClickAndHoldCommandProperty =
        DependencyProperty.Register("ClickAndHoldCommand", typeof(ICommand), typeof(HoldButton), new UIPropertyMetadata(null));

    // Using a DependencyProperty as the backing store for ClickCommand.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ClickCommandProperty =
        DependencyProperty.Register("ClickCommand", typeof(ICommand), typeof(HoldButton), new UIPropertyMetadata(null));

    // Using a DependencyProperty as the backing store for EnableClickHold.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EnableClickHoldProperty =
        DependencyProperty.Register("EnableClickHold", typeof(bool), typeof(HoldButton), new PropertyMetadata(false));

    // Using a DependencyProperty as the backing store for MillisecondsToWait.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MillisecondsToWaitProperty =
        DependencyProperty.Register("MillisecondsToWait", typeof(int), typeof(HoldButton), new PropertyMetadata(0));

    public int MillisecondsToWait
    {
        get { return (int)GetValue(MillisecondsToWaitProperty); }
        set { SetValue(MillisecondsToWaitProperty, value); }
    }

    private int numberButtonRepeats;

    private void OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (EnableClickHold)
        {
            numberButtonRepeats = 0;

            if(!buttonIsHeldPressed)
            {
                ClickCommand?.Execute(this.CommandParameter);
            }

            buttonIsHeldPressed = false;
        }
    }

    private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Trace.WriteLine("OnPreviewMouseLeftButtonDown()");
        if (EnableClickHold)
        {
            // When numberButtonRepeats comes above 1 then the button is considered to be pressed long
            if (numberButtonRepeats > 1)
            {
                ClickAndHoldCommand?.Execute(this.CommandParameter);
            }

            numberButtonRepeats++;
        }
    }
}
<UserControl x:Class="Views.TemperatureControlView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Views" 
             xmlns:cal="http://www.caliburnproject.org"
             xmlns:controls="clr-namespace:UserControls"
             mc:Ignorable="d" 
             d:DesignHeight="250" d:DesignWidth="150">
    <Slider Minimum="{Binding MinimumTemperature}" 
            Maximum="{Binding MaximumTemperature}" 
            SmallChange="{Binding TemperatureTinySteps}" 
            LargeChange="{Binding TemperatureSmallSteps}" 
            Value="{Binding ControlValue}" 
            Style="{StaticResource NumericUpDown}" />
</UserControl>
RBCSharp
  • 17
  • 5
  • Better extend the Slider control and implement the logic in the new class. – BionicCode Nov 29 '21 at 12:32
  • I am not sure if the trigger is going to work since the Value, Minimum and Maximum are floats – RBCSharp Nov 29 '21 at 13:03
  • You must extend Slider. In the OnApplyTemplate override you grab the up and down RepeatButtons. Then register a property changed callback for the Value property. In this callback you disable either the up or the down button depending on the current Value compared to Minimum and Maximum. – BionicCode Nov 29 '21 at 13:06
  • Can you perhaps help me a little with the OnApplyTemplate()? Where do i have to place this? I added the ValueChanged event handler to the partial class but there i need to get to the up and down button. I like an mvvm way of doing this... – RBCSharp Nov 29 '21 at 13:27
  • See my answer, please. It's 100% MVVM compliant. – BionicCode Nov 29 '21 at 13:37

1 Answers1

1

You should extend the Slider control and implement the logic there.
Finally name the RepeatButton elements and move the Style to the Generic.xaml file.

public class CustomSlider : Slider
{
  static CustomSlider()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomSlider), new FrameworkPropertyMetadata(typeof(CustomSlider)));
  }

  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
    this.PART_IncreaseButton = GetTemplateChild(nameof(this.PART_IncreaseButton)) as UIElement;
    this.PART_DecreaseButton = GetTemplateChild(nameof(this.PART_DecreaseButton)) as UIElement;
  }

  protected override void OnValueChanged(double oldValue, double newValue)
  {
    base.OnValueChanged(oldValue, newValue);

    if (this.PART_IncreaseButton == null 
      || this.PART_DecreaseButton == null)
    {
      return;
    }

    this.PART_IncreaseButton.IsEnabled = newValue < this.Maximum;
    this.PART_DecreaseButton.IsEnabled = newValue > this.Minimum;
  }

  private UIElement PART_IncreaseButton { get; set; }
  private UIElement PART_DecreaseButton { get; set; }
}

Generic.xaml
Name the HoldButton elements "PART_IncreaseButton" and "PART_IncreaseButton" so that you can find them easily in the template.

<Style TargetType="{x:Type CustomSlider}">
  <Style.Resources>
    <Style x:Key="RepeatButtonStyle" TargetType="{x:Type RepeatButton}">
      <Setter Property="Focusable" Value="false" />
      <Setter Property="IsTabStop" Value="false" />
      <Setter Property="Padding" Value="0" />
      <Setter Property="Width" Value="20" />
    </Style>
  </Style.Resources>
  <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false" />
  <Setter Property="SmallChange" Value="1" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Slider}">
        <Grid>
          <Rectangle RadiusX="10" RadiusY="10" Stroke="{StaticResource SolidBrushLightGrey}" Fill="Black" StrokeThickness="1" />
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
              
            <TextBlock Grid.Row="0" x:Name="ControlName" Style="{StaticResource LabelStyle}" Margin="0,5,0,0" />
            <TextBlock Grid.Row="1" x:Name="ControlUnits" Style="{StaticResource LabelStyle}" Margin="0,5,0,0" />
              
            <usercontrols:HoldButton x:Name="PART_IncreaseButton" 
                                     Grid.Row="2" 
                                     Delay="250" 
                                     Interval="375" 
                                     EnableClickHold="True" 
                                     ClickAndHoldCommand="{x:Static Slider.IncreaseLarge}" 
                                     ClickCommand="{x:Static Slider.IncreaseSmall}"
                                     MaxWidth="60" 
                                     Height="60" Width="60" 
                                     Style="{StaticResource ButtonStyleGeneral}" 
                                     Content="+" />


            <TextBlock Grid.Row="3" x:Name="Temperature" Style="{StaticResource LabelStyle}" Margin="0,5,0,0" FontSize="30" Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Value, StringFormat=N1}" />

            <usercontrols:HoldButton x:Name="PART_DecreaseButton" 
                                     Grid.Row="4" 
                                     Delay="250" 
                                     Interval="375" 
                                     EnableClickHold="True" 
                                     ClickAndHoldCommand="{x:Static Slider.DecreaseLarge}" 
                                     ClickCommand="{x:Static Slider.DecreaseSmall}" 
                                     MaxWidth="60" 
                                     Height="60" Width="60" 
                                     Style="{StaticResource ButtonStyleGeneral}" 
                                     Content="-" />


              <Border x:Name="TrackBackground" Visibility="Collapsed">
              <Rectangle x:Name="PART_SelectionRange" Visibility="Collapsed" />
            </Border>
            <Thumb x:Name="Thumb" Visibility="Collapsed" />
          </Grid>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thank you for the answer but i am experiencing a problem with a NullReferenceException in OnValueChanged(). It seems that the OnValueChanged() handler is executed before OnApplyTemplate() and therefor the button objects are null. – RBCSharp Nov 29 '21 at 13:48
  • I have not tested the code. It's written in the text editor too. But I can guarantee that it works. I have added a null check. – BionicCode Nov 29 '21 at 13:53
  • Added a null reference check for the buttons, then it works perfectly. Thank you for your time and help. – RBCSharp Nov 29 '21 at 13:56