xceedsoftware / wpftoolkit

All the controls missing in WPF. Over 1 million downloads.
Other
3.91k stars 878 forks source link

NumericUpDown: Shouldn't there be a single event for when the user made a change that should be acted upon? #1750

Open cwenger opened 1 year ago

cwenger commented 1 year ago

When the user is editing a NumericUpDown, every keystroke raises a ValueChanged event. This is not ideal when it's linked to something that you don't want changed more frequently than necessary. In the past I have dealt with this by handling the KeyUp, LostFocus, and Spinned events like this:

private void TheIntegerUpDown_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter || e.Key == Key.Return)
        if (TheIntegerUpDown.Value.HasValue)
            Update(TheIntegerUpDown.Value);
}

private void TheIntegerUpDown_LostFocus(object sender, RoutedEventArgs e)
{
    if (TheIntegerUpDown.Value.HasValue)
        Update(TheIntegerUpDown.Value);
}

private void TheIntegerUpDown_Spinned(object sender, Xceed.Wpf.Toolkit.SpinEventArgs e)
{
    if (TheIntegerUpDown.Value.HasValue && TheIntegerUpDown.Increment.HasValue)
    {
        if (e.Direction == Xceed.Wpf.Toolkit.SpinDirection.Increase)
            Update(TheIntegerUpDown.Value + TheIntegerUpDown.Increment);
        else if (e.Direction == Xceed.Wpf.Toolkit.SpinDirection.Decrease)
            Update(TheIntegerUpDown.Value - TheIntegerUpDown.Increment);
    }
}

This seem like a lot of code to handle something conceptually very simple. I see there is an UpdateValueOnEnterKey property which could simplify this a bit, but when that's set to true, using the up/down spinner buttons no longer raises the ValueChanged event. Furthermore, let's say you needed to look at multiple NumericUpDowns whenever a spinner button on one is used. You would need some mechanism to indicate one particular NumericUpDown's new value, while the others are unchanged.

I can envision a few ways of solving this, if an elegant solution does not exist already:

XceedDanP commented 1 year ago

Have you tried the Delay property on the Binding object?

BindingBase.Delay Property (System.Windows.Data) | Microsoft Learnhttps://learn.microsoft.com/en-us/dotnet/api/system.windows.data.bindingbase.delay?view=netframework-4.8

This is a value in milliseconds to wait until the input from the user is sent to the binding source.


From: Craig Wenger @.> Sent: Saturday, June 3, 2023 1:32 PM To: xceedsoftware/wpftoolkit @.> Cc: Subscribed @.***> Subject: [xceedsoftware/wpftoolkit] NumericUpDown: Shouldn't there be a single event for when the user made a change that should be acted upon? (Issue #1750)

When the user is editing a NumericUpDown, every keystroke raises a ValueChanged event. This is not ideal when it's linked to something that you don't want changed more frequently than necessary. In the past I have dealt with this by handling the KeyUp, LostFocus, and Spinned events like this:

private void TheIntegerUpDown_KeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.Enter || e.Key == Key.Return) if (TheIntegerUpDown.Value.HasValue) Update(TheIntegerUpDown.Value); }

private void TheIntegerUpDown_LostFocus(object sender, RoutedEventArgs e) { if (TheIntegerUpDown.Value.HasValue) Update(TheIntegerUpDown.Value); }

private void TheIntegerUpDown_Spinned(object sender, Xceed.Wpf.Toolkit.SpinEventArgs e) { if (TheIntegerUpDown.Value.HasValue && TheIntegerUpDown.Increment.HasValue) { if (e.Direction == Xceed.Wpf.Toolkit.SpinDirection.Increase) Update(TheIntegerUpDown.Value + TheIntegerUpDown.Increment); else if (e.Direction == Xceed.Wpf.Toolkit.SpinDirection.Decrease) Update(TheIntegerUpDown.Value - TheIntegerUpDown.Increment); } }

This seem like a lot of code to handle something conceptually very simple. I see there is an UpdateValueOnEnterKey property which could simplify this a bit, but when that's set to true, using the up/down spinner buttons no longer raises the ValueChanged event. Furthermore, let's say you needed to look at multiple NumericUpDowns whenever a spinner button on one is used. You would need some mechanism to indicate one particular NumericUpDown's new value, while the others are unchanged.

I can envision a few ways of solving this, if an elegant solution does not exist already:

— Reply to this email directly, view it on GitHubhttps://github.com/xceedsoftware/wpftoolkit/issues/1750, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A7M3JW4OL7JV7LY4XPVGOWTXJNYMXANCNFSM6AAAAAAYZNDR54. You are receiving this because you are subscribed to this thread.Message ID: @.***>

cwenger commented 1 year ago

@XceedDanP I'm not using binding.

XceedBoucherS commented 1 year ago

Hi,

Currently, yes, every typed value in the TextBox of the NumericUpDown raises a ValueChanged event. The same when a spinner button is clicked.

If you set the "UpdateValueOnEnterKey" property to True, then typing values in the TextBox of the NumericUpDown won't raise the ValueChanged event. Also, as you pointed out, clicking the Spinner buttons won't raise a ValueChanged event because we are considered in an edit mode, until a lost focus or Enter key press.

If you want to use the advantage of the "UpdateValueOnEnterKey" property to raise a ValueChanged on Enter key press and lost focus, but also on Spinner button clicks, you could: -Set the "UpdateValueOnEnterKey" to True -Add a callback for DoubleUpDown.Spinned and in the callback, call doubleUpDown.CommitInput() to sync the Value and Text property (this will produce a change on the Value property and raise the ValueChange event):

` <xctk:DoubleUpDown Value="25" UpdateValueOnEnterKey="True" ValueChanged="DoubleUpDown_ValueChanged" Spinned="DoubleUpDown_Spinned"/> private void DoubleUpDown_ValueChanged( object sender, RoutedPropertyChangedEventArgs e ) { System.Diagnostics.Debug.WriteLine( " Change " ); }

private void DoubleUpDown_Spinned( object sender, Xceed.Wpf.Toolkit.SpinEventArgs e )
{
  if( sender is DoubleUpDown doubleUpDown )
  {
    doubleUpDown.CommitInput();
  }
}`

Please note that the Spinned event will tell you which NumericUpDown is being spinned(with the sender) and if it's an Increase or a Decrease(with the SpinEventArgs). The ValueChanged event will tell you which NumericUpDown's Value is being changed(with the sender) and the old and new value(with the RoutedPropertyChangedEventArgs).

cwenger commented 1 year ago

This seems like a reasonable solution, but for some reason the Spinned event is not propagating to a ValueChanged event until focus is changed. So if I output e.NewValue in the ValueChanged event handler, it's actually showing the old value. Using essentially the same code as yours:

        private void TheDoubleUpDown_ValueChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            System.Diagnostics.Debug.WriteLine($"value changed to {e.NewValue}");
        }

        private void TheDoubleUpDown_Spinned(object sender, Xceed.Wpf.Toolkit.SpinEventArgs e)
        {
            if (sender is DoubleUpDown dud)
                dud.CommitInput();
        }

Note that if you put a breakpoint inside the Spinned event it changes the focus, making the behavior seem correct.

EDIT: Could also be related to the Spinned event being raised before the value is changed instead of after. But for reasons I don't understand, even adding a breakpoint on the very last end brace of the Spinned event handler also gives the desired behavior.

cwenger commented 1 year ago

Here is one possible solution, adding a short asynchronous delay inside the Spinned event to allow the value to be updated before CommitInput() is called. But it feels pretty hacky with an arbitrary 10 ms delay.

        private void TheDoubleUpDown_ValueChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            System.Diagnostics.Debug.WriteLine($"value changed to {e.NewValue}");
        }

        private async void TheDoubleUpDown_Spinned(object sender, Xceed.Wpf.Toolkit.SpinEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("spinned");
            if (sender is DoubleUpDown dud)
            {
                await Task.Delay(10);
                dud.CommitInput();
            }
        }
XceedBoucherS commented 1 year ago

Hi,

This is happening because if the Value is "25", on a click of a Spinner, the Spinned event is raised. In the callback, you call doubleUpDown.CommitInput(); which takes the content of the TextBoxBox ("25") and put it in the "DoubleUpDown.Value" and then a ValueChanged event is raised(with "25"). When this is done, the actual incrementation from the Spinner click is done : in your case, setting the TextBox.Text to "26".

This will be fixed in v4.6. In the meantime, if you have the source code, you can go in file Xceed.Wpf.Toolkit/Primitives/UpDownBase.cs, in method : virtual void OnSpin( SpinEventArgs e ) and move the following code at the end of the method: // Raise the Spinned event to user EventHandler handler = this.Spinned; if( handler != null ) { handler( this, e ); } This will force an incrementation (in your case Update the TextBox.Text property) on a spinner click and then raise the Spinned event (where your callback will put TextBox.Text in DoubleUpDown.Value, resulting in a ValueChanged event with the new value).

Thank you