From 9a90238178940334902deeceb9610f5e8a66878a Mon Sep 17 00:00:00 2001 From: Harald Daltveit Date: Sat, 13 Jun 2026 01:32:11 +0200 Subject: [PATCH] fix(DoubleUpDown): revert to previous value on out-of-range input with ClipValueToMinMax=false When ClipValueToMinMax=false and the user enters an out-of-range value then tabs away (UpdateSourceTrigger=LostFocus), the control was silently truncating digits to bring the value within Min/Max range. For example, with Max=20000, typing 123456 would set the VM property to 12345 instead of reverting to the original valid value. Root cause: The intermediate valid values typed (e.g. 12345) were being committed to the internal Value property during typing. On LostFocus, when the final value (123456) was out of range, the text was reverted to the last committed intermediate value instead of the pre-edit value. Fix: Save the initial Value on focus gain (_initialValueOnFocus), and restore it in the SyncTextAndValueProperties catch block when ClipValueToMinMax=false and the user is not actively typing. Fixes #1801 --- .../Primitives/UpDownBase.cs | 1875 +++++++++-------- 1 file changed, 945 insertions(+), 930 deletions(-) diff --git a/ExtendedWPFToolkitSolution/Src/Xceed.Wpf.Toolkit/Primitives/UpDownBase.cs b/ExtendedWPFToolkitSolution/Src/Xceed.Wpf.Toolkit/Primitives/UpDownBase.cs index 9d1b6e6a..2eaff822 100644 --- a/ExtendedWPFToolkitSolution/Src/Xceed.Wpf.Toolkit/Primitives/UpDownBase.cs +++ b/ExtendedWPFToolkitSolution/Src/Xceed.Wpf.Toolkit/Primitives/UpDownBase.cs @@ -1,930 +1,945 @@ -/************************************************************************************* - - Toolkit for WPF - - Copyright (C) 2007-2025 Xceed Software Inc. - - This program is provided to you under the terms of the XCEED SOFTWARE, INC. - COMMUNITY LICENSE AGREEMENT (for non-commercial use) as published at - https://github.com/xceedsoftware/wpftoolkit/blob/master/license.md - - For more features, controls, and fast professional support, - pick up the Plus Edition at https://xceed.com/xceed-toolkit-plus-for-wpf/ - - Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids - - ***********************************************************************************/ - -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Input; -using System.Windows.Threading; -using Xceed.Wpf.Toolkit.Core.Input; - -namespace Xceed.Wpf.Toolkit.Primitives -{ - [TemplatePart( Name = PART_TextBox, Type = typeof( TextBox ) )] - [TemplatePart( Name = PART_Spinner, Type = typeof( Spinner ) )] - public abstract class UpDownBase : InputBase, IValidateInput - { - #region Members - - internal const string PART_TextBox = "PART_TextBox"; - - internal const string PART_Spinner = "PART_Spinner"; - - internal bool _isTextChangedFromUI; - - private bool _isSyncingTextAndValueProperties; - private bool _internalValueSet; - - #endregion //Members - - #region Properties - - protected Spinner Spinner - { - get; - private set; - } - protected TextBox TextBox - { - get; - private set; - } - - #region AllowSpin - - public static readonly DependencyProperty AllowSpinProperty = DependencyProperty.Register( "AllowSpin", typeof( bool ), typeof( UpDownBase ), - -new UIPropertyMetadata( true ) ); - public bool AllowSpin - { - get - { - return ( bool )GetValue( AllowSpinProperty ); - } - set - { - SetValue( AllowSpinProperty, value ); - } - } - - #endregion //AllowSpin - - #region ButtonSpinnerHeight - - public static readonly DependencyProperty ButtonSpinnerHeightProperty = DependencyProperty.Register( "ButtonSpinnerHeight", typeof( double ), typeof( UpDownBase ), new UIPropertyMetadata( double.NaN ) ); - public double ButtonSpinnerHeight - { - get - { - return ( double )GetValue( ButtonSpinnerHeightProperty ); - } - set - { - SetValue( ButtonSpinnerHeightProperty, value ); - } - } - - #endregion //ButtonSpinnerHeight - - #region ButtonSpinnerLocation - - public static readonly DependencyProperty ButtonSpinnerLocationProperty = DependencyProperty.Register( "ButtonSpinnerLocation", typeof( Location - -), typeof( UpDownBase ), new UIPropertyMetadata( Location.Right ) ); - public Location ButtonSpinnerLocation - { - get - { - return ( Location )GetValue( ButtonSpinnerLocationProperty ); - } - set - { - SetValue( ButtonSpinnerLocationProperty, value ); - } - } - - #endregion //ButtonSpinnerLocation - - #region ButtonSpinnerDownContentTemplate - - public static readonly DependencyProperty ButtonSpinnerDownContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerDownContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); - public DataTemplate ButtonSpinnerDownContentTemplate - { - get - { - return ( DataTemplate )GetValue( ButtonSpinnerDownContentTemplateProperty ); - } - set - { - SetValue( ButtonSpinnerDownContentTemplateProperty, value ); - } - } - - #endregion //ButtonSpinnerDownContentTemplate - - #region ButtonSpinnerDownDisabledContentTemplate - - public static readonly DependencyProperty ButtonSpinnerDownDisabledContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerDownDisabledContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); - public DataTemplate ButtonSpinnerDownDisabledContentTemplate - { - get - { - return ( DataTemplate )GetValue( ButtonSpinnerDownDisabledContentTemplateProperty ); - } - set - { - SetValue( ButtonSpinnerDownDisabledContentTemplateProperty, value ); - } - } - - #endregion //ButtonSpinnerDownDisabledContentTemplate - - #region ButtonSpinnerUpContentTemplate - - public static readonly DependencyProperty ButtonSpinnerUpContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerUpContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); - public DataTemplate ButtonSpinnerUpContentTemplate - { - get - { - return ( DataTemplate )GetValue( ButtonSpinnerUpContentTemplateProperty ); - } - set - { - SetValue( ButtonSpinnerUpContentTemplateProperty, value ); - } - } - - #endregion //ButtonSpinnerUpContentTemplate - - #region ButtonSpinnerUpDisabledContentTemplate - - public static readonly DependencyProperty ButtonSpinnerUpDisabledContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerUpDisabledContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); - public DataTemplate ButtonSpinnerUpDisabledContentTemplate - { - get - { - return ( DataTemplate )GetValue( ButtonSpinnerUpDisabledContentTemplateProperty ); - } - set - { - SetValue( ButtonSpinnerUpDisabledContentTemplateProperty, value ); - } - } - - #endregion //ButtonSpinnerUpDisabledContentTemplate - - #region ButtonSpinnerWidth - - public static readonly DependencyProperty ButtonSpinnerWidthProperty = DependencyProperty.Register( "ButtonSpinnerWidth", typeof( double ), typeof( UpDownBase ), new UIPropertyMetadata( SystemParameters.VerticalScrollBarWidth ) ); - public double ButtonSpinnerWidth - { - get - { - return ( double )GetValue( ButtonSpinnerWidthProperty ); - } - set - { - SetValue( ButtonSpinnerWidthProperty, value ); - } - } - - #endregion //ButtonSpinnerWidth - - #region ClipValueToMinMax - - public static readonly DependencyProperty ClipValueToMinMaxProperty = DependencyProperty.Register( "ClipValueToMinMax", typeof( bool ), typeof( - -UpDownBase ), new UIPropertyMetadata( false ) ); - public bool ClipValueToMinMax - { - get - { - return ( bool )GetValue( ClipValueToMinMaxProperty ); - } - set - { - SetValue( ClipValueToMinMaxProperty, value ); - } - } - - #endregion //ClipValueToMinMax - - #region DisplayDefaultValueOnEmptyText - - public static readonly DependencyProperty DisplayDefaultValueOnEmptyTextProperty = DependencyProperty.Register( "DisplayDefaultValueOnEmptyText", - -typeof( bool ), typeof( UpDownBase ), new UIPropertyMetadata( false, OnDisplayDefaultValueOnEmptyTextChanged ) ); - public bool DisplayDefaultValueOnEmptyText - { - get - { - return ( bool )GetValue( DisplayDefaultValueOnEmptyTextProperty ); - } - set - { - SetValue( DisplayDefaultValueOnEmptyTextProperty, value ); - } - } - - private static void OnDisplayDefaultValueOnEmptyTextChanged( DependencyObject source, DependencyPropertyChangedEventArgs args ) - { - ( ( UpDownBase )source ).OnDisplayDefaultValueOnEmptyTextChanged( ( bool )args.OldValue, ( bool )args.NewValue ); - } - - private void OnDisplayDefaultValueOnEmptyTextChanged( bool oldValue, bool newValue ) - { - if( this.IsInitialized && string.IsNullOrEmpty( Text ) ) - { - this.SyncTextAndValueProperties( false, Text ); - } - } - - #endregion //DisplayDefaultValueOnEmptyText - - #region DefaultValue - - public static readonly DependencyProperty DefaultValueProperty = DependencyProperty.Register( "DefaultValue", typeof( T ), typeof( UpDownBase - -), new UIPropertyMetadata( default( T ), OnDefaultValueChanged ) ); - public T DefaultValue - { - get - { - return ( T )GetValue( DefaultValueProperty ); - } - set - { - SetValue( DefaultValueProperty, value ); - } - } - - private static void OnDefaultValueChanged( DependencyObject source, DependencyPropertyChangedEventArgs args ) - { - ( ( UpDownBase )source ).OnDefaultValueChanged( ( T )args.OldValue, ( T )args.NewValue ); - } - - private void OnDefaultValueChanged( T oldValue, T newValue ) - { - if( this.IsInitialized && string.IsNullOrEmpty( Text ) ) - { - this.SyncTextAndValueProperties( true, Text ); - } - } - - #endregion //DefaultValue - - #region Maximum - - public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register( "Maximum", typeof( T ), typeof( UpDownBase ), new - -UIPropertyMetadata( default( T ), OnMaximumChanged, OnCoerceMaximum ) ); - public T Maximum - { - get - { - return ( T )GetValue( MaximumProperty ); - } - set - { - SetValue( MaximumProperty, value ); - } - } - - private static void OnMaximumChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) - { - UpDownBase upDown = o as UpDownBase; - if( upDown != null ) - upDown.OnMaximumChanged( ( T )e.OldValue, ( T )e.NewValue ); - } - - protected virtual void OnMaximumChanged( T oldValue, T newValue ) - { - if( this.IsInitialized ) - { - SetValidSpinDirection(); - } - } - - private static object OnCoerceMaximum( DependencyObject d, object baseValue ) - { - UpDownBase upDown = d as UpDownBase; - if( upDown != null ) - return upDown.OnCoerceMaximum( ( T )baseValue ); - - return baseValue; - } - - protected virtual T OnCoerceMaximum( T baseValue ) - { - return baseValue; - } - - #endregion //Maximum - - #region Minimum - - public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register( "Minimum", typeof( T ), typeof( UpDownBase ), new - -UIPropertyMetadata( default( T ), OnMinimumChanged, OnCoerceMinimum ) ); - public T Minimum - { - get - { - return ( T )GetValue( MinimumProperty ); - } - set - { - SetValue( MinimumProperty, value ); - } - } - - private static void OnMinimumChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) - { - UpDownBase upDown = o as UpDownBase; - if( upDown != null ) - upDown.OnMinimumChanged( ( T )e.OldValue, ( T )e.NewValue ); - } - - protected virtual void OnMinimumChanged( T oldValue, T newValue ) - { - if( this.IsInitialized ) - { - SetValidSpinDirection(); - } - } - - private static object OnCoerceMinimum( DependencyObject d, object baseValue ) - { - UpDownBase upDown = d as UpDownBase; - if( upDown != null ) - return upDown.OnCoerceMinimum( ( T )baseValue ); - - return baseValue; - } - - protected virtual T OnCoerceMinimum( T baseValue ) - { - return baseValue; - } - - #endregion //Minimum - - #region MouseWheelActiveTrigger - - public static readonly DependencyProperty MouseWheelActiveTriggerProperty = DependencyProperty.Register( "MouseWheelActiveTrigger", typeof( - -MouseWheelActiveTrigger ), typeof( UpDownBase ), new UIPropertyMetadata( MouseWheelActiveTrigger.FocusedMouseOver ) ); - - public MouseWheelActiveTrigger MouseWheelActiveTrigger - { - get - { - return ( MouseWheelActiveTrigger )GetValue( MouseWheelActiveTriggerProperty ); - } - set - { - SetValue( MouseWheelActiveTriggerProperty, value ); - } - } - - #endregion //MouseWheelActiveTrigger - - #region MouseWheelActiveOnFocus - - [Obsolete( "Use MouseWheelActiveTrigger property instead" )] - public static readonly DependencyProperty MouseWheelActiveOnFocusProperty = DependencyProperty.Register( "MouseWheelActiveOnFocus", typeof( bool - -), typeof( UpDownBase ), new UIPropertyMetadata( true, OnMouseWheelActiveOnFocusChanged ) ); - - [Obsolete( "Use MouseWheelActiveTrigger property instead" )] - public bool MouseWheelActiveOnFocus - { - get - { -#pragma warning disable 618 - return ( bool )GetValue( MouseWheelActiveOnFocusProperty ); -#pragma warning restore 618 - } - set - { -#pragma warning disable 618 - SetValue( MouseWheelActiveOnFocusProperty, value ); -#pragma warning restore 618 - } - } - - private static void OnMouseWheelActiveOnFocusChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) - { - UpDownBase upDownBase = o as UpDownBase; - if( upDownBase != null ) - upDownBase.MouseWheelActiveTrigger = ( ( bool )e.NewValue ) - ? MouseWheelActiveTrigger.FocusedMouseOver - : MouseWheelActiveTrigger.MouseOver; - } - - #endregion //MouseWheelActiveOnFocus - - #region ShowButtonSpinner - - public static readonly DependencyProperty ShowButtonSpinnerProperty = DependencyProperty.Register( "ShowButtonSpinner", typeof( bool ), typeof( - -UpDownBase ), new UIPropertyMetadata( true ) ); - public bool ShowButtonSpinner - { - get - { - return ( bool )GetValue( ShowButtonSpinnerProperty ); - } - set - { - SetValue( ShowButtonSpinnerProperty, value ); - } - } - - #endregion //ShowButtonSpinner - - #region UpdateValueOnEnterKey - - public static readonly DependencyProperty UpdateValueOnEnterKeyProperty = DependencyProperty.Register( "UpdateValueOnEnterKey", typeof( bool ), typeof( UpDownBase ), - new FrameworkPropertyMetadata( false, OnUpdateValueOnEnterKeyChanged ) ); - public bool UpdateValueOnEnterKey - { - get - { - return ( bool )GetValue( UpdateValueOnEnterKeyProperty ); - } - set - { - SetValue( UpdateValueOnEnterKeyProperty, value ); - } - } - - private static void OnUpdateValueOnEnterKeyChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) - { - var upDownBase = o as UpDownBase; - if( upDownBase != null ) - upDownBase.OnUpdateValueOnEnterKeyChanged( ( bool )e.OldValue, ( bool )e.NewValue ); - } - - protected virtual void OnUpdateValueOnEnterKeyChanged( bool oldValue, bool newValue ) - { - } - - #endregion //UpdateValueOnEnterKey - - #region Value - - public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof( T ), typeof( UpDownBase ), - new FrameworkPropertyMetadata( default( T ), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged, OnCoerceValue, false, UpdateSourceTrigger.PropertyChanged ) ); - public T Value - { - get - { - return ( T )GetValue( ValueProperty ); - } - set - { - SetValue( ValueProperty, value ); - } - } - - private void SetValueInternal( T value ) - { - _internalValueSet = true; - try - { - this.Value = value; - } - finally - { - _internalValueSet = false; - } - } - - private static object OnCoerceValue( DependencyObject o, object basevalue ) - { - return ( ( UpDownBase )o ).OnCoerceValue( basevalue ); - } - - protected virtual object OnCoerceValue( object newValue ) - { - return newValue; - } - - private static void OnValueChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) - { - UpDownBase upDownBase = o as UpDownBase; - if( upDownBase != null ) - upDownBase.OnValueChanged( ( T )e.OldValue, ( T )e.NewValue ); - } - - protected virtual void OnValueChanged( T oldValue, T newValue ) - { - if( !_internalValueSet && this.IsInitialized ) - { - SyncTextAndValueProperties( false, null, true ); - } - - SetValidSpinDirection(); - - this.RaiseValueChangedEvent( oldValue, newValue ); - } - - #endregion //Value - - #endregion //Properties - - #region Constructors - - internal UpDownBase() - { - - Core.Message.ShowMessage(); - - this.AddHandler( Mouse.PreviewMouseDownOutsideCapturedElementEvent, new RoutedEventHandler( this.HandleClickOutsideOfControlWithMouseCapture ), - -true ); - this.IsKeyboardFocusWithinChanged += this.UpDownBase_IsKeyboardFocusWithinChanged; - } - - #endregion //Constructors - - #region Base Class Overrides - - protected override void OnAccessKey( AccessKeyEventArgs e ) - { - if( TextBox != null ) - TextBox.Focus(); - - base.OnAccessKey( e ); - } - - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - if( TextBox != null ) - { - TextBox.TextChanged -= new TextChangedEventHandler( TextBox_TextChanged ); - TextBox.RemoveHandler( Mouse.PreviewMouseDownEvent, new MouseButtonEventHandler( this.TextBox_PreviewMouseDown ) ); - } - - TextBox = GetTemplateChild( PART_TextBox ) as TextBox; - - if( TextBox != null ) - { - TextBox.Text = Text; - TextBox.TextChanged += new TextChangedEventHandler( TextBox_TextChanged ); - TextBox.AddHandler( Mouse.PreviewMouseDownEvent, new MouseButtonEventHandler( this.TextBox_PreviewMouseDown ), true ); - } - - if( Spinner != null ) - Spinner.Spin -= OnSpinnerSpin; - - Spinner = GetTemplateChild( PART_Spinner ) as Spinner; - - if( Spinner != null ) - Spinner.Spin += OnSpinnerSpin; - - SetValidSpinDirection(); - } - - protected override void OnKeyDown( KeyEventArgs e ) - { - switch( e.Key ) - { - case Key.Enter: - { - // Commit Text on "Enter" to raise Error event - bool commitSuccess = CommitInput(); - //Only handle if an exception is detected (Commit fails) - e.Handled = !commitSuccess; - break; - } - } - } - - protected override void OnTextChanged( string oldValue, string newValue ) - { - if( this.IsInitialized ) - { - // When text is typed, if UpdateValueOnEnterKey is true, - // Sync Value on Text only when Enter Key is pressed. - if( this.UpdateValueOnEnterKey ) - { - if( !_isTextChangedFromUI ) - { - this.SyncTextAndValueProperties( true, Text ); - } - } - else - { - this.SyncTextAndValueProperties( true, Text ); - } - } - } - - protected override void OnCultureInfoChanged( CultureInfo oldValue, CultureInfo newValue ) - { - if( IsInitialized ) - { - SyncTextAndValueProperties( false, null ); - } - } - - protected override void OnReadOnlyChanged( bool oldValue, bool newValue ) - { - SetValidSpinDirection(); - } - - #endregion //Base Class Overrides - - #region Event Handlers - - private void TextBox_PreviewMouseDown( object sender, RoutedEventArgs e ) - { - if( this.MouseWheelActiveTrigger == Primitives.MouseWheelActiveTrigger.Focused ) - { - //Capture the spinner when user clicks on the control. - if( Mouse.Captured != this.Spinner ) - { - //Delay the capture to let the DateTimeUpDown select a new DateTime part. - this.Dispatcher.BeginInvoke( DispatcherPriority.Input, new Action( () => - { - Mouse.Capture( this.Spinner ); - } - ) ); - } - } - } - - private void HandleClickOutsideOfControlWithMouseCapture( object sender, RoutedEventArgs e ) - { - if( Mouse.Captured is Spinner ) - { - //Release the captured spinner when user clicks away from spinner. - this.Spinner.ReleaseMouseCapture(); - } - } - - private void OnSpinnerSpin( object sender, SpinEventArgs e ) - { - if( AllowSpin && !IsReadOnly ) - { - var activeTrigger = this.MouseWheelActiveTrigger; - bool spin = !e.UsingMouseWheel; - spin |= ( activeTrigger == MouseWheelActiveTrigger.MouseOver ); - spin |= ( ( TextBox != null ) && TextBox.IsFocused && ( activeTrigger == MouseWheelActiveTrigger.FocusedMouseOver ) ); - spin |= ( ( TextBox != null ) && TextBox.IsFocused && ( activeTrigger == MouseWheelActiveTrigger.Focused ) && ( Mouse.Captured is Spinner ) ); - - if( spin ) - { - e.Handled = true; - OnSpin( e ); - } - } - } - - #endregion //Event Handlers - - #region Events - - public event InputValidationErrorEventHandler InputValidationError; - - public event EventHandler Spinned; - - #region ValueChanged Event - - //Due to a bug in Visual Studio, you cannot create event handlers for generic T args in XAML, so I have to use object instead. - public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( "ValueChanged", RoutingStrategy.Bubble, typeof( - -RoutedPropertyChangedEventHandler ), typeof( UpDownBase ) ); - public event RoutedPropertyChangedEventHandler ValueChanged - { - add - { - AddHandler( ValueChangedEvent, value ); - } - remove - { - RemoveHandler( ValueChangedEvent, value ); - } - } - - #endregion - - #endregion //Events - - #region Methods - - protected virtual void OnSpin( SpinEventArgs e ) - { - if( e == null ) - throw new ArgumentNullException( "e" ); - - if( e.Direction == SpinDirection.Increase ) - { - this.DoIncrement(); - } - else - { - this.DoDecrement(); - } - - // Raise the Spinned event to user - EventHandler handler = this.Spinned; - if( handler != null ) - { - handler( this, e ); - } - } - - protected virtual void RaiseValueChangedEvent( T oldValue, T newValue ) - { - RoutedPropertyChangedEventArgs args = new RoutedPropertyChangedEventArgs( oldValue, newValue ); - args.RoutedEvent = ValueChangedEvent; - RaiseEvent( args ); - } - - protected override void OnInitialized( EventArgs e ) - { - base.OnInitialized( e ); - // When both Value and Text are initialized, Value has priority. - // To be sure that the value is not initialized, it should - // have no local value, no binding, and equal to the default value. - bool updateValueFromText = - ( this.ReadLocalValue( ValueProperty ) == DependencyProperty.UnsetValue ) - && ( BindingOperations.GetBinding( this, ValueProperty ) == null ) - && ( object.Equals( this.Value, ValueProperty.DefaultMetadata.DefaultValue ) ); - - this.SyncTextAndValueProperties( updateValueFromText, Text, !updateValueFromText ); - } - - internal void DoDecrement() - { - if( Spinner == null || ( Spinner.ValidSpinDirection & ValidSpinDirections.Decrease ) == ValidSpinDirections.Decrease ) - { - OnDecrement(); - } - } - - internal void DoIncrement() - { - if( Spinner == null || ( Spinner.ValidSpinDirection & ValidSpinDirections.Increase ) == ValidSpinDirections.Increase ) - { - OnIncrement(); - } - } - - private void TextBox_TextChanged( object sender, TextChangedEventArgs e ) - { - if( !this.IsKeyboardFocusWithin ) - return; - - try - { - _isTextChangedFromUI = true; - Text = ( ( TextBox )sender ).Text; - } - finally - { - _isTextChangedFromUI = false; - } - } - - private void UpDownBase_IsKeyboardFocusWithinChanged( object sender, DependencyPropertyChangedEventArgs e ) - { - if( !( bool )e.NewValue ) - { - this.CommitInput(); - } - } - - private void RaiseInputValidationError( Exception e ) - { - if( InputValidationError != null ) - { - InputValidationErrorEventArgs args = new InputValidationErrorEventArgs( e ); - InputValidationError( this, args ); - if( args.ThrowException ) - { - throw args.Exception; - } - } - } - - public virtual bool CommitInput() - { - return this.SyncTextAndValueProperties( true, Text ); - } - - protected bool SyncTextAndValueProperties( bool updateValueFromText, string text ) - { - return this.SyncTextAndValueProperties( updateValueFromText, text, false ); - } - - private bool SyncTextAndValueProperties( bool updateValueFromText, string text, bool forceTextUpdate ) - { - if( _isSyncingTextAndValueProperties ) - return true; - - _isSyncingTextAndValueProperties = true; - bool parsedTextIsValid = true; - try - { - if( updateValueFromText ) - { - if( string.IsNullOrEmpty( text ) ) - { - // An empty input sets the value to the default value. - this.SetValueInternal( this.DefaultValue ); - } - else - { - try - { - T newValue = this.ConvertTextToValue( text ); - if( !object.Equals( newValue, this.Value ) ) - { - this.SetValueInternal( newValue ); - } - } - catch( Exception e ) - { - parsedTextIsValid = false; - - // From the UI, just allow any input. - if( !_isTextChangedFromUI ) - { - // This call may throw an exception. - // See RaiseInputValidationError() implementation. - this.RaiseInputValidationError( e ); - } - } - } - } - - // Do not touch the ongoing text input from user. - if( !_isTextChangedFromUI ) - { - // Don't replace the empty Text with the non-empty representation of DefaultValue. - bool shouldKeepEmpty = !forceTextUpdate && string.IsNullOrEmpty( Text ) && object.Equals( Value, DefaultValue ) && !this.DisplayDefaultValueOnEmptyText; - if( !shouldKeepEmpty ) - { - string newText = ConvertValueToText(); - if( !object.Equals( this.Text, newText ) ) - { - Text = newText; - } - } - - // Sync Text and textBox - if( TextBox != null ) - TextBox.Text = Text; - } - - if( _isTextChangedFromUI && !parsedTextIsValid ) - { - // Text input was made from the user and the text - // repesents an invalid value. Disable the spinner - // in this case. - if( Spinner != null ) - { - Spinner.ValidSpinDirection = ValidSpinDirections.None; - } - } - else - { - this.SetValidSpinDirection(); - } - } - finally - { - _isSyncingTextAndValueProperties = false; - } - return parsedTextIsValid; - } - - #region Abstract - - protected abstract T ConvertTextToValue( string text ); - - protected abstract string ConvertValueToText(); - - protected abstract void OnIncrement(); - - protected abstract void OnDecrement(); - - protected abstract void SetValidSpinDirection(); - - #endregion //Abstract - - #endregion //Methods - } -} +/************************************************************************************* + + Toolkit for WPF + + Copyright (C) 2007-2025 Xceed Software Inc. + + This program is provided to you under the terms of the XCEED SOFTWARE, INC. + COMMUNITY LICENSE AGREEMENT (for non-commercial use) as published at + https://github.com/xceedsoftware/wpftoolkit/blob/master/license.md + + For more features, controls, and fast professional support, + pick up the Plus Edition at https://xceed.com/xceed-toolkit-plus-for-wpf/ + + Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids + + ***********************************************************************************/ + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Threading; +using Xceed.Wpf.Toolkit.Core.Input; + +namespace Xceed.Wpf.Toolkit.Primitives +{ + [TemplatePart( Name = PART_TextBox, Type = typeof( TextBox ) )] + [TemplatePart( Name = PART_Spinner, Type = typeof( Spinner ) )] + public abstract class UpDownBase : InputBase, IValidateInput + { + #region Members + + internal const string PART_TextBox = "PART_TextBox"; + + internal const string PART_Spinner = "PART_Spinner"; + + internal bool _isTextChangedFromUI; + private T _initialValueOnFocus; + + private bool _isSyncingTextAndValueProperties; + private bool _internalValueSet; + + #endregion //Members + + #region Properties + + protected Spinner Spinner + { + get; + private set; + } + protected TextBox TextBox + { + get; + private set; + } + + #region AllowSpin + + public static readonly DependencyProperty AllowSpinProperty = DependencyProperty.Register( "AllowSpin", typeof( bool ), typeof( UpDownBase ), + +new UIPropertyMetadata( true ) ); + public bool AllowSpin + { + get + { + return ( bool )GetValue( AllowSpinProperty ); + } + set + { + SetValue( AllowSpinProperty, value ); + } + } + + #endregion //AllowSpin + + #region ButtonSpinnerHeight + + public static readonly DependencyProperty ButtonSpinnerHeightProperty = DependencyProperty.Register( "ButtonSpinnerHeight", typeof( double ), typeof( UpDownBase ), new UIPropertyMetadata( double.NaN ) ); + public double ButtonSpinnerHeight + { + get + { + return ( double )GetValue( ButtonSpinnerHeightProperty ); + } + set + { + SetValue( ButtonSpinnerHeightProperty, value ); + } + } + + #endregion //ButtonSpinnerHeight + + #region ButtonSpinnerLocation + + public static readonly DependencyProperty ButtonSpinnerLocationProperty = DependencyProperty.Register( "ButtonSpinnerLocation", typeof( Location + +), typeof( UpDownBase ), new UIPropertyMetadata( Location.Right ) ); + public Location ButtonSpinnerLocation + { + get + { + return ( Location )GetValue( ButtonSpinnerLocationProperty ); + } + set + { + SetValue( ButtonSpinnerLocationProperty, value ); + } + } + + #endregion //ButtonSpinnerLocation + + #region ButtonSpinnerDownContentTemplate + + public static readonly DependencyProperty ButtonSpinnerDownContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerDownContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); + public DataTemplate ButtonSpinnerDownContentTemplate + { + get + { + return ( DataTemplate )GetValue( ButtonSpinnerDownContentTemplateProperty ); + } + set + { + SetValue( ButtonSpinnerDownContentTemplateProperty, value ); + } + } + + #endregion //ButtonSpinnerDownContentTemplate + + #region ButtonSpinnerDownDisabledContentTemplate + + public static readonly DependencyProperty ButtonSpinnerDownDisabledContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerDownDisabledContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); + public DataTemplate ButtonSpinnerDownDisabledContentTemplate + { + get + { + return ( DataTemplate )GetValue( ButtonSpinnerDownDisabledContentTemplateProperty ); + } + set + { + SetValue( ButtonSpinnerDownDisabledContentTemplateProperty, value ); + } + } + + #endregion //ButtonSpinnerDownDisabledContentTemplate + + #region ButtonSpinnerUpContentTemplate + + public static readonly DependencyProperty ButtonSpinnerUpContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerUpContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); + public DataTemplate ButtonSpinnerUpContentTemplate + { + get + { + return ( DataTemplate )GetValue( ButtonSpinnerUpContentTemplateProperty ); + } + set + { + SetValue( ButtonSpinnerUpContentTemplateProperty, value ); + } + } + + #endregion //ButtonSpinnerUpContentTemplate + + #region ButtonSpinnerUpDisabledContentTemplate + + public static readonly DependencyProperty ButtonSpinnerUpDisabledContentTemplateProperty = DependencyProperty.Register( "ButtonSpinnerUpDisabledContentTemplate", typeof( DataTemplate ), typeof( UpDownBase ), new UIPropertyMetadata( null ) ); + public DataTemplate ButtonSpinnerUpDisabledContentTemplate + { + get + { + return ( DataTemplate )GetValue( ButtonSpinnerUpDisabledContentTemplateProperty ); + } + set + { + SetValue( ButtonSpinnerUpDisabledContentTemplateProperty, value ); + } + } + + #endregion //ButtonSpinnerUpDisabledContentTemplate + + #region ButtonSpinnerWidth + + public static readonly DependencyProperty ButtonSpinnerWidthProperty = DependencyProperty.Register( "ButtonSpinnerWidth", typeof( double ), typeof( UpDownBase ), new UIPropertyMetadata( SystemParameters.VerticalScrollBarWidth ) ); + public double ButtonSpinnerWidth + { + get + { + return ( double )GetValue( ButtonSpinnerWidthProperty ); + } + set + { + SetValue( ButtonSpinnerWidthProperty, value ); + } + } + + #endregion //ButtonSpinnerWidth + + #region ClipValueToMinMax + + public static readonly DependencyProperty ClipValueToMinMaxProperty = DependencyProperty.Register( "ClipValueToMinMax", typeof( bool ), typeof( + +UpDownBase ), new UIPropertyMetadata( false ) ); + public bool ClipValueToMinMax + { + get + { + return ( bool )GetValue( ClipValueToMinMaxProperty ); + } + set + { + SetValue( ClipValueToMinMaxProperty, value ); + } + } + + #endregion //ClipValueToMinMax + + #region DisplayDefaultValueOnEmptyText + + public static readonly DependencyProperty DisplayDefaultValueOnEmptyTextProperty = DependencyProperty.Register( "DisplayDefaultValueOnEmptyText", + +typeof( bool ), typeof( UpDownBase ), new UIPropertyMetadata( false, OnDisplayDefaultValueOnEmptyTextChanged ) ); + public bool DisplayDefaultValueOnEmptyText + { + get + { + return ( bool )GetValue( DisplayDefaultValueOnEmptyTextProperty ); + } + set + { + SetValue( DisplayDefaultValueOnEmptyTextProperty, value ); + } + } + + private static void OnDisplayDefaultValueOnEmptyTextChanged( DependencyObject source, DependencyPropertyChangedEventArgs args ) + { + ( ( UpDownBase )source ).OnDisplayDefaultValueOnEmptyTextChanged( ( bool )args.OldValue, ( bool )args.NewValue ); + } + + private void OnDisplayDefaultValueOnEmptyTextChanged( bool oldValue, bool newValue ) + { + if( this.IsInitialized && string.IsNullOrEmpty( Text ) ) + { + this.SyncTextAndValueProperties( false, Text ); + } + } + + #endregion //DisplayDefaultValueOnEmptyText + + #region DefaultValue + + public static readonly DependencyProperty DefaultValueProperty = DependencyProperty.Register( "DefaultValue", typeof( T ), typeof( UpDownBase + +), new UIPropertyMetadata( default( T ), OnDefaultValueChanged ) ); + public T DefaultValue + { + get + { + return ( T )GetValue( DefaultValueProperty ); + } + set + { + SetValue( DefaultValueProperty, value ); + } + } + + private static void OnDefaultValueChanged( DependencyObject source, DependencyPropertyChangedEventArgs args ) + { + ( ( UpDownBase )source ).OnDefaultValueChanged( ( T )args.OldValue, ( T )args.NewValue ); + } + + private void OnDefaultValueChanged( T oldValue, T newValue ) + { + if( this.IsInitialized && string.IsNullOrEmpty( Text ) ) + { + this.SyncTextAndValueProperties( true, Text ); + } + } + + #endregion //DefaultValue + + #region Maximum + + public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register( "Maximum", typeof( T ), typeof( UpDownBase ), new + +UIPropertyMetadata( default( T ), OnMaximumChanged, OnCoerceMaximum ) ); + public T Maximum + { + get + { + return ( T )GetValue( MaximumProperty ); + } + set + { + SetValue( MaximumProperty, value ); + } + } + + private static void OnMaximumChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) + { + UpDownBase upDown = o as UpDownBase; + if( upDown != null ) + upDown.OnMaximumChanged( ( T )e.OldValue, ( T )e.NewValue ); + } + + protected virtual void OnMaximumChanged( T oldValue, T newValue ) + { + if( this.IsInitialized ) + { + SetValidSpinDirection(); + } + } + + private static object OnCoerceMaximum( DependencyObject d, object baseValue ) + { + UpDownBase upDown = d as UpDownBase; + if( upDown != null ) + return upDown.OnCoerceMaximum( ( T )baseValue ); + + return baseValue; + } + + protected virtual T OnCoerceMaximum( T baseValue ) + { + return baseValue; + } + + #endregion //Maximum + + #region Minimum + + public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register( "Minimum", typeof( T ), typeof( UpDownBase ), new + +UIPropertyMetadata( default( T ), OnMinimumChanged, OnCoerceMinimum ) ); + public T Minimum + { + get + { + return ( T )GetValue( MinimumProperty ); + } + set + { + SetValue( MinimumProperty, value ); + } + } + + private static void OnMinimumChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) + { + UpDownBase upDown = o as UpDownBase; + if( upDown != null ) + upDown.OnMinimumChanged( ( T )e.OldValue, ( T )e.NewValue ); + } + + protected virtual void OnMinimumChanged( T oldValue, T newValue ) + { + if( this.IsInitialized ) + { + SetValidSpinDirection(); + } + } + + private static object OnCoerceMinimum( DependencyObject d, object baseValue ) + { + UpDownBase upDown = d as UpDownBase; + if( upDown != null ) + return upDown.OnCoerceMinimum( ( T )baseValue ); + + return baseValue; + } + + protected virtual T OnCoerceMinimum( T baseValue ) + { + return baseValue; + } + + #endregion //Minimum + + #region MouseWheelActiveTrigger + + public static readonly DependencyProperty MouseWheelActiveTriggerProperty = DependencyProperty.Register( "MouseWheelActiveTrigger", typeof( + +MouseWheelActiveTrigger ), typeof( UpDownBase ), new UIPropertyMetadata( MouseWheelActiveTrigger.FocusedMouseOver ) ); + + public MouseWheelActiveTrigger MouseWheelActiveTrigger + { + get + { + return ( MouseWheelActiveTrigger )GetValue( MouseWheelActiveTriggerProperty ); + } + set + { + SetValue( MouseWheelActiveTriggerProperty, value ); + } + } + + #endregion //MouseWheelActiveTrigger + + #region MouseWheelActiveOnFocus + + [Obsolete( "Use MouseWheelActiveTrigger property instead" )] + public static readonly DependencyProperty MouseWheelActiveOnFocusProperty = DependencyProperty.Register( "MouseWheelActiveOnFocus", typeof( bool + +), typeof( UpDownBase ), new UIPropertyMetadata( true, OnMouseWheelActiveOnFocusChanged ) ); + + [Obsolete( "Use MouseWheelActiveTrigger property instead" )] + public bool MouseWheelActiveOnFocus + { + get + { +#pragma warning disable 618 + return ( bool )GetValue( MouseWheelActiveOnFocusProperty ); +#pragma warning restore 618 + } + set + { +#pragma warning disable 618 + SetValue( MouseWheelActiveOnFocusProperty, value ); +#pragma warning restore 618 + } + } + + private static void OnMouseWheelActiveOnFocusChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) + { + UpDownBase upDownBase = o as UpDownBase; + if( upDownBase != null ) + upDownBase.MouseWheelActiveTrigger = ( ( bool )e.NewValue ) + ? MouseWheelActiveTrigger.FocusedMouseOver + : MouseWheelActiveTrigger.MouseOver; + } + + #endregion //MouseWheelActiveOnFocus + + #region ShowButtonSpinner + + public static readonly DependencyProperty ShowButtonSpinnerProperty = DependencyProperty.Register( "ShowButtonSpinner", typeof( bool ), typeof( + +UpDownBase ), new UIPropertyMetadata( true ) ); + public bool ShowButtonSpinner + { + get + { + return ( bool )GetValue( ShowButtonSpinnerProperty ); + } + set + { + SetValue( ShowButtonSpinnerProperty, value ); + } + } + + #endregion //ShowButtonSpinner + + #region UpdateValueOnEnterKey + + public static readonly DependencyProperty UpdateValueOnEnterKeyProperty = DependencyProperty.Register( "UpdateValueOnEnterKey", typeof( bool ), typeof( UpDownBase ), + new FrameworkPropertyMetadata( false, OnUpdateValueOnEnterKeyChanged ) ); + public bool UpdateValueOnEnterKey + { + get + { + return ( bool )GetValue( UpdateValueOnEnterKeyProperty ); + } + set + { + SetValue( UpdateValueOnEnterKeyProperty, value ); + } + } + + private static void OnUpdateValueOnEnterKeyChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) + { + var upDownBase = o as UpDownBase; + if( upDownBase != null ) + upDownBase.OnUpdateValueOnEnterKeyChanged( ( bool )e.OldValue, ( bool )e.NewValue ); + } + + protected virtual void OnUpdateValueOnEnterKeyChanged( bool oldValue, bool newValue ) + { + } + + #endregion //UpdateValueOnEnterKey + + #region Value + + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof( T ), typeof( UpDownBase ), + new FrameworkPropertyMetadata( default( T ), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged, OnCoerceValue, false, UpdateSourceTrigger.PropertyChanged ) ); + public T Value + { + get + { + return ( T )GetValue( ValueProperty ); + } + set + { + SetValue( ValueProperty, value ); + } + } + + private void SetValueInternal( T value ) + { + _internalValueSet = true; + try + { + this.Value = value; + } + finally + { + _internalValueSet = false; + } + } + + private static object OnCoerceValue( DependencyObject o, object basevalue ) + { + return ( ( UpDownBase )o ).OnCoerceValue( basevalue ); + } + + protected virtual object OnCoerceValue( object newValue ) + { + return newValue; + } + + private static void OnValueChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) + { + UpDownBase upDownBase = o as UpDownBase; + if( upDownBase != null ) + upDownBase.OnValueChanged( ( T )e.OldValue, ( T )e.NewValue ); + } + + protected virtual void OnValueChanged( T oldValue, T newValue ) + { + if( !_internalValueSet && this.IsInitialized ) + { + SyncTextAndValueProperties( false, null, true ); + } + + SetValidSpinDirection(); + + this.RaiseValueChangedEvent( oldValue, newValue ); + } + + #endregion //Value + + #endregion //Properties + + #region Constructors + + internal UpDownBase() + { + + Core.Message.ShowMessage(); + + this.AddHandler( Mouse.PreviewMouseDownOutsideCapturedElementEvent, new RoutedEventHandler( this.HandleClickOutsideOfControlWithMouseCapture ), + +true ); + this.IsKeyboardFocusWithinChanged += this.UpDownBase_IsKeyboardFocusWithinChanged; + } + + #endregion //Constructors + + #region Base Class Overrides + + protected override void OnAccessKey( AccessKeyEventArgs e ) + { + if( TextBox != null ) + TextBox.Focus(); + + base.OnAccessKey( e ); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if( TextBox != null ) + { + TextBox.TextChanged -= new TextChangedEventHandler( TextBox_TextChanged ); + TextBox.RemoveHandler( Mouse.PreviewMouseDownEvent, new MouseButtonEventHandler( this.TextBox_PreviewMouseDown ) ); + } + + TextBox = GetTemplateChild( PART_TextBox ) as TextBox; + + if( TextBox != null ) + { + TextBox.Text = Text; + TextBox.TextChanged += new TextChangedEventHandler( TextBox_TextChanged ); + TextBox.AddHandler( Mouse.PreviewMouseDownEvent, new MouseButtonEventHandler( this.TextBox_PreviewMouseDown ), true ); + } + + if( Spinner != null ) + Spinner.Spin -= OnSpinnerSpin; + + Spinner = GetTemplateChild( PART_Spinner ) as Spinner; + + if( Spinner != null ) + Spinner.Spin += OnSpinnerSpin; + + SetValidSpinDirection(); + } + + protected override void OnKeyDown( KeyEventArgs e ) + { + switch( e.Key ) + { + case Key.Enter: + { + // Commit Text on "Enter" to raise Error event + bool commitSuccess = CommitInput(); + //Only handle if an exception is detected (Commit fails) + e.Handled = !commitSuccess; + break; + } + } + } + + protected override void OnTextChanged( string oldValue, string newValue ) + { + if( this.IsInitialized ) + { + // When text is typed, if UpdateValueOnEnterKey is true, + // Sync Value on Text only when Enter Key is pressed. + if( this.UpdateValueOnEnterKey ) + { + if( !_isTextChangedFromUI ) + { + this.SyncTextAndValueProperties( true, Text ); + } + } + else + { + this.SyncTextAndValueProperties( true, Text ); + } + } + } + + protected override void OnCultureInfoChanged( CultureInfo oldValue, CultureInfo newValue ) + { + if( IsInitialized ) + { + SyncTextAndValueProperties( false, null ); + } + } + + protected override void OnReadOnlyChanged( bool oldValue, bool newValue ) + { + SetValidSpinDirection(); + } + + #endregion //Base Class Overrides + + #region Event Handlers + + private void TextBox_PreviewMouseDown( object sender, RoutedEventArgs e ) + { + if( this.MouseWheelActiveTrigger == Primitives.MouseWheelActiveTrigger.Focused ) + { + //Capture the spinner when user clicks on the control. + if( Mouse.Captured != this.Spinner ) + { + //Delay the capture to let the DateTimeUpDown select a new DateTime part. + this.Dispatcher.BeginInvoke( DispatcherPriority.Input, new Action( () => + { + Mouse.Capture( this.Spinner ); + } + ) ); + } + } + } + + private void HandleClickOutsideOfControlWithMouseCapture( object sender, RoutedEventArgs e ) + { + if( Mouse.Captured is Spinner ) + { + //Release the captured spinner when user clicks away from spinner. + this.Spinner.ReleaseMouseCapture(); + } + } + + private void OnSpinnerSpin( object sender, SpinEventArgs e ) + { + if( AllowSpin && !IsReadOnly ) + { + var activeTrigger = this.MouseWheelActiveTrigger; + bool spin = !e.UsingMouseWheel; + spin |= ( activeTrigger == MouseWheelActiveTrigger.MouseOver ); + spin |= ( ( TextBox != null ) && TextBox.IsFocused && ( activeTrigger == MouseWheelActiveTrigger.FocusedMouseOver ) ); + spin |= ( ( TextBox != null ) && TextBox.IsFocused && ( activeTrigger == MouseWheelActiveTrigger.Focused ) && ( Mouse.Captured is Spinner ) ); + + if( spin ) + { + e.Handled = true; + OnSpin( e ); + } + } + } + + #endregion //Event Handlers + + #region Events + + public event InputValidationErrorEventHandler InputValidationError; + + public event EventHandler Spinned; + + #region ValueChanged Event + + //Due to a bug in Visual Studio, you cannot create event handlers for generic T args in XAML, so I have to use object instead. + public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( "ValueChanged", RoutingStrategy.Bubble, typeof( + +RoutedPropertyChangedEventHandler ), typeof( UpDownBase ) ); + public event RoutedPropertyChangedEventHandler ValueChanged + { + add + { + AddHandler( ValueChangedEvent, value ); + } + remove + { + RemoveHandler( ValueChangedEvent, value ); + } + } + + #endregion + + #endregion //Events + + #region Methods + + protected virtual void OnSpin( SpinEventArgs e ) + { + if( e == null ) + throw new ArgumentNullException( "e" ); + + if( e.Direction == SpinDirection.Increase ) + { + this.DoIncrement(); + } + else + { + this.DoDecrement(); + } + + // Raise the Spinned event to user + EventHandler handler = this.Spinned; + if( handler != null ) + { + handler( this, e ); + } + } + + protected virtual void RaiseValueChangedEvent( T oldValue, T newValue ) + { + RoutedPropertyChangedEventArgs args = new RoutedPropertyChangedEventArgs( oldValue, newValue ); + args.RoutedEvent = ValueChangedEvent; + RaiseEvent( args ); + } + + protected override void OnInitialized( EventArgs e ) + { + base.OnInitialized( e ); + // When both Value and Text are initialized, Value has priority. + // To be sure that the value is not initialized, it should + // have no local value, no binding, and equal to the default value. + bool updateValueFromText = + ( this.ReadLocalValue( ValueProperty ) == DependencyProperty.UnsetValue ) + && ( BindingOperations.GetBinding( this, ValueProperty ) == null ) + && ( object.Equals( this.Value, ValueProperty.DefaultMetadata.DefaultValue ) ); + + this.SyncTextAndValueProperties( updateValueFromText, Text, !updateValueFromText ); + } + + internal void DoDecrement() + { + if( Spinner == null || ( Spinner.ValidSpinDirection & ValidSpinDirections.Decrease ) == ValidSpinDirections.Decrease ) + { + OnDecrement(); + } + } + + internal void DoIncrement() + { + if( Spinner == null || ( Spinner.ValidSpinDirection & ValidSpinDirections.Increase ) == ValidSpinDirections.Increase ) + { + OnIncrement(); + } + } + + private void TextBox_TextChanged( object sender, TextChangedEventArgs e ) + { + if( !this.IsKeyboardFocusWithin ) + return; + + try + { + _isTextChangedFromUI = true; + Text = ( ( TextBox )sender ).Text; + } + finally + { + _isTextChangedFromUI = false; + } + } + + private void UpDownBase_IsKeyboardFocusWithinChanged( object sender, DependencyPropertyChangedEventArgs e ) + { + if( ( bool )e.NewValue ) + { + // Save the current value when focus is gained so we can restore it + // if the user enters an out-of-range value with ClipValueToMinMax=false. + _initialValueOnFocus = this.Value; + } + else + { + this.CommitInput(); + } + } + + private void RaiseInputValidationError( Exception e ) + { + if( InputValidationError != null ) + { + InputValidationErrorEventArgs args = new InputValidationErrorEventArgs( e ); + InputValidationError( this, args ); + if( args.ThrowException ) + { + throw args.Exception; + } + } + } + + public virtual bool CommitInput() + { + return this.SyncTextAndValueProperties( true, Text ); + } + + protected bool SyncTextAndValueProperties( bool updateValueFromText, string text ) + { + return this.SyncTextAndValueProperties( updateValueFromText, text, false ); + } + + private bool SyncTextAndValueProperties( bool updateValueFromText, string text, bool forceTextUpdate ) + { + if( _isSyncingTextAndValueProperties ) + return true; + + _isSyncingTextAndValueProperties = true; + bool parsedTextIsValid = true; + try + { + if( updateValueFromText ) + { + if( string.IsNullOrEmpty( text ) ) + { + // An empty input sets the value to the default value. + this.SetValueInternal( this.DefaultValue ); + } + else + { + try + { + T newValue = this.ConvertTextToValue( text ); + if( !object.Equals( newValue, this.Value ) ) + { + this.SetValueInternal( newValue ); + } + } + catch( Exception e ) + { + parsedTextIsValid = false; + + // From the UI, just allow any input. + if( !_isTextChangedFromUI ) + { + // If ClipValueToMinMax is disabled, restore the original value + // when the user enters an out-of-range value, instead of + // silently truncating digits to fit the range. + if( !this.ClipValueToMinMax ) + { + this.SetValueInternal( _initialValueOnFocus ); + } + + // This call may throw an exception. + // See RaiseInputValidationError() implementation. + this.RaiseInputValidationError( e ); + } + } + } + } + + // Do not touch the ongoing text input from user. + if( !_isTextChangedFromUI ) + { + // Don't replace the empty Text with the non-empty representation of DefaultValue. + bool shouldKeepEmpty = !forceTextUpdate && string.IsNullOrEmpty( Text ) && object.Equals( Value, DefaultValue ) && !this.DisplayDefaultValueOnEmptyText; + if( !shouldKeepEmpty ) + { + string newText = ConvertValueToText(); + if( !object.Equals( this.Text, newText ) ) + { + Text = newText; + } + } + + // Sync Text and textBox + if( TextBox != null ) + TextBox.Text = Text; + } + + if( _isTextChangedFromUI && !parsedTextIsValid ) + { + // Text input was made from the user and the text + // repesents an invalid value. Disable the spinner + // in this case. + if( Spinner != null ) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + } + else + { + this.SetValidSpinDirection(); + } + } + finally + { + _isSyncingTextAndValueProperties = false; + } + return parsedTextIsValid; + } + + #region Abstract + + protected abstract T ConvertTextToValue( string text ); + + protected abstract string ConvertValueToText(); + + protected abstract void OnIncrement(); + + protected abstract void OnDecrement(); + + protected abstract void SetValidSpinDirection(); + + #endregion //Abstract + + #endregion //Methods + } +}