/************************************************************************************* Extended WPF Toolkit Copyright (C) 2007-2013 Xceed Software Inc. This program is provided to you under the terms of the Microsoft Public License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license For more features, controls, and fast professional support, pick up the Plus Edition at http://xceed.com/wpf_toolkit Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids ***********************************************************************************/ using System; using System.Windows; using System.Globalization; using System.IO; using System.Linq; using Xceed.Wpf.Toolkit.Primitives; namespace Xceed.Wpf.Toolkit { public abstract class CommonNumericUpDown : NumericUpDown where T : struct, IFormattable, IComparable { protected delegate bool FromText( string s, NumberStyles style, IFormatProvider provider, out T result ); protected delegate T FromDecimal( decimal d ); private FromText _fromText; private FromDecimal _fromDecimal; private Func _fromLowerThan; private Func _fromGreaterThan; #region ParsingNumberStyle public static readonly DependencyProperty ParsingNumberStyleProperty = DependencyProperty.Register( "ParsingNumberStyle", typeof( NumberStyles ), typeof( CommonNumericUpDown ), new UIPropertyMetadata( NumberStyles.Any ) ); public NumberStyles ParsingNumberStyle { get { return ( NumberStyles )GetValue( ParsingNumberStyleProperty ); } set { SetValue( ParsingNumberStyleProperty, value ); } } #endregion //ParsingNumberStyle #region Constructors protected CommonNumericUpDown( FromText fromText, FromDecimal fromDecimal, Func fromLowerThan, Func fromGreaterThan ) { if( fromText == null ) throw new ArgumentNullException( "tryParseMethod" ); if( fromDecimal == null ) throw new ArgumentNullException( "fromDecimal" ); if( fromLowerThan == null ) throw new ArgumentNullException( "fromLowerThan" ); if( fromGreaterThan == null ) throw new ArgumentNullException( "fromGreaterThan" ); _fromText = fromText; _fromDecimal = fromDecimal; _fromLowerThan = fromLowerThan; _fromGreaterThan = fromGreaterThan; } #endregion protected static void UpdateMetadata( Type type, T? increment, T? minValue, T? maxValue ) { DefaultStyleKeyProperty.OverrideMetadata( type, new FrameworkPropertyMetadata( type ) ); UpdateMetadataCommon( type, increment, minValue, maxValue ); } private static void UpdateMetadataCommon( Type type, T? increment, T? minValue, T? maxValue ) { IncrementProperty.OverrideMetadata( type, new FrameworkPropertyMetadata( increment ) ); MaximumProperty.OverrideMetadata( type, new FrameworkPropertyMetadata( maxValue ) ); MinimumProperty.OverrideMetadata( type, new FrameworkPropertyMetadata( minValue ) ); } protected void TestInputSpecialValue( AllowedSpecialValues allowedValues, AllowedSpecialValues valueToCompare ) { if( ( allowedValues & valueToCompare ) != valueToCompare ) { switch( valueToCompare ) { case AllowedSpecialValues.NaN : throw new InvalidDataException( "Value to parse shouldn't be NaN." ); case AllowedSpecialValues.PositiveInfinity: throw new InvalidDataException( "Value to parse shouldn't be Positive Infinity." ); case AllowedSpecialValues.NegativeInfinity: throw new InvalidDataException( "Value to parse shouldn't be Negative Infinity." ); } } } private bool IsLowerThan( T? value1, T? value2 ) { if( value1 == null || value2 == null ) return false; return _fromLowerThan( value1.Value, value2.Value ); } private bool IsGreaterThan( T? value1, T? value2 ) { if( value1 == null || value2 == null ) return false; return _fromGreaterThan( value1.Value, value2.Value ); } private bool HandleNullSpin() { if( !Value.HasValue ) { T forcedValue = ( DefaultValue.HasValue ) ? DefaultValue.Value : default( T ); Value = CoerceValueMinMax( forcedValue ); return true; } else if( !Increment.HasValue ) { return true; } return false; } internal bool IsValid( T? value ) { return !IsLowerThan( value, Minimum ) && !IsGreaterThan( value, Maximum ); } private T? CoerceValueMinMax( T value ) { if( IsLowerThan( value, Minimum ) ) return Minimum; else if( IsGreaterThan( value, Maximum ) ) return Maximum; else return value; } #region Base Class Overrides protected override void OnIncrement() { if( !HandleNullSpin() ) { // if UpdateValueOnEnterKey is true, // Sync Value on Text only when Enter Key is pressed. if( this.UpdateValueOnEnterKey ) { var currentValue = this.ConvertTextToValue( this.TextBox.Text ); var result = this.IncrementValue( currentValue.Value, Increment.Value ); var newValue = this.CoerceValueMinMax( result ); this.TextBox.Text = newValue.Value.ToString( this.FormatString, this.CultureInfo ); } else { var result = this.IncrementValue( Value.Value, Increment.Value ); this.Value = this.CoerceValueMinMax( result ); } } } protected override void OnDecrement() { if( !HandleNullSpin() ) { // if UpdateValueOnEnterKey is true, // Sync Value on Text only when Enter Key is pressed. if( this.UpdateValueOnEnterKey ) { var currentValue = this.ConvertTextToValue( this.TextBox.Text ); var result = this.DecrementValue( currentValue.Value, Increment.Value ); var newValue = this.CoerceValueMinMax( result ); this.TextBox.Text = newValue.Value.ToString( this.FormatString, this.CultureInfo ); } else { var result = this.DecrementValue( Value.Value, Increment.Value ); this.Value = this.CoerceValueMinMax( result ); } } } protected override void OnMinimumChanged( T? oldValue, T? newValue ) { base.OnMinimumChanged( oldValue, newValue ); if( this.Value.HasValue && this.ClipValueToMinMax ) { this.Value = this.CoerceValueMinMax( this.Value.Value ); } } protected override void OnMaximumChanged( T? oldValue, T? newValue ) { base.OnMaximumChanged( oldValue, newValue ); if( this.Value.HasValue && this.ClipValueToMinMax ) { this.Value = this.CoerceValueMinMax( this.Value.Value ); } } protected override T? ConvertTextToValue( string text ) { T? result = null; if( String.IsNullOrEmpty( text ) ) return result; // Since the conversion from Value to text using a FormartString may not be parsable, // we verify that the already existing text is not the exact same value. string currentValueText = ConvertValueToText(); if( object.Equals( currentValueText, text ) ) return this.Value; result = this.ConvertTextToValueCore( currentValueText, text ); if( this.ClipValueToMinMax ) { return this.GetClippedMinMaxValue( result ); } ValidateDefaultMinMax( result ); return result; } protected override string ConvertValueToText() { if( Value == null ) return string.Empty; //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. if( FormatString.Contains( "{0" ) ) return string.Format( CultureInfo, FormatString, Value.Value ); return Value.Value.ToString( FormatString, CultureInfo ); } protected override void SetValidSpinDirection() { ValidSpinDirections validDirections = ValidSpinDirections.None; // Null increment always prevents spin. if( (this.Increment != null) && !IsReadOnly ) { if( IsLowerThan( Value, Maximum ) || !Value.HasValue || !Maximum.HasValue) validDirections = validDirections | ValidSpinDirections.Increase; if( IsGreaterThan( Value, Minimum ) || !Value.HasValue || !Minimum.HasValue ) validDirections = validDirections | ValidSpinDirections.Decrease; } if( Spinner != null ) Spinner.ValidSpinDirection = validDirections; } private bool IsPercent( string stringToTest ) { int PIndex = stringToTest.IndexOf( "P" ); if( PIndex >= 0 ) { //stringToTest contains a "P" between 2 "'", it's considered as text, not percent bool isText = (stringToTest.Substring( 0, PIndex ).Contains( "'" ) && stringToTest.Substring( PIndex, FormatString.Length - PIndex ).Contains( "'" )); return !isText; } return false; } private T? ConvertTextToValueCore( string currentValueText, string text ) { T? result; if( this.IsPercent( this.FormatString ) ) { result = _fromDecimal( ParsePercent( text, CultureInfo ) ); } else { T outputValue = new T(); // Problem while converting new text if( !_fromText( text, this.ParsingNumberStyle, CultureInfo, out outputValue ) ) { bool shouldThrow = true; // case 164198: Throw when replacing only the digit part of 99° through UI. // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° T currentValueTextOutputValue; if( !_fromText( currentValueText, this.ParsingNumberStyle, CultureInfo, out currentValueTextOutputValue ) ) { // extract non-digit characters var currentValueTextSpecialCharacters = currentValueText.Where( c => !Char.IsDigit( c ) ); var textSpecialCharacters = text.Where( c => !Char.IsDigit( c ) ); // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. if( currentValueTextSpecialCharacters.Except( textSpecialCharacters ).ToList().Count == 0 ) { foreach( var character in textSpecialCharacters ) { text = text.Replace( character.ToString(), string.Empty ); } // if without the special characters, parsing is good, do not throw if( _fromText( text, this.ParsingNumberStyle, CultureInfo, out outputValue ) ) { shouldThrow = false; } } } if( shouldThrow ) throw new InvalidDataException( "Input string was not in a correct format." ); } result = outputValue; } return result; } private T? GetClippedMinMaxValue( T? result ) { if( this.IsGreaterThan( result, this.Maximum ) ) return this.Maximum; else if( this.IsLowerThan( result, this.Minimum ) ) return this.Minimum; return result; } private void ValidateDefaultMinMax( T? value ) { // DefaultValue is always accepted. if( object.Equals( value, DefaultValue ) ) return; if( IsLowerThan( value, Minimum ) ) throw new ArgumentOutOfRangeException( "Minimum", String.Format( "Value must be greater than MinValue of {0}", Minimum ) ); else if( IsGreaterThan( value, Maximum ) ) throw new ArgumentOutOfRangeException( "Maximum", String.Format( "Value must be less than MaxValue of {0}", Maximum ) ); } #endregion //Base Class Overrides #region Abstract Methods protected abstract T IncrementValue( T value, T increment ); protected abstract T DecrementValue( T value, T increment ); #endregion } }