/************************************************************************************* 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.Collections.Generic; using System.Globalization; using System.Windows; using System.Windows.Data; using System.Windows.Input; using Xceed.Wpf.Toolkit.Primitives; using System.Windows.Controls; using System.Linq; using Xceed.Wpf.Toolkit.Core.Utilities; namespace Xceed.Wpf.Toolkit { public class DateTimeUpDown : DateTimeUpDownBase { #region Members private DateTime? _lastValidDate; //null private bool _setKindInternal = false; #endregion #region Properties #region Format public static readonly DependencyProperty FormatProperty = DependencyProperty.Register( "Format", typeof( DateTimeFormat ), typeof( DateTimeUpDown ), new UIPropertyMetadata( DateTimeFormat.FullDateTime, OnFormatChanged ) ); public DateTimeFormat Format { get { return ( DateTimeFormat )GetValue( FormatProperty ); } set { SetValue( FormatProperty, value ); } } private static void OnFormatChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { DateTimeUpDown dateTimeUpDown = o as DateTimeUpDown; if( dateTimeUpDown != null ) dateTimeUpDown.OnFormatChanged( ( DateTimeFormat )e.OldValue, ( DateTimeFormat )e.NewValue ); } protected virtual void OnFormatChanged( DateTimeFormat oldValue, DateTimeFormat newValue ) { FormatUpdated(); } #endregion //Format #region FormatString public static readonly DependencyProperty FormatStringProperty = DependencyProperty.Register( "FormatString", typeof( string ), typeof( DateTimeUpDown ), new UIPropertyMetadata( default( String ), OnFormatStringChanged ), IsFormatStringValid ); public string FormatString { get { return ( string )GetValue( FormatStringProperty ); } set { SetValue( FormatStringProperty, value ); } } internal static bool IsFormatStringValid( object value ) { try { // Test the format string if it is used. DateTime.MinValue.ToString( ( string )value, CultureInfo.CurrentCulture ); } catch { return false; } return true; } private static void OnFormatStringChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { DateTimeUpDown dateTimeUpDown = o as DateTimeUpDown; if( dateTimeUpDown != null ) dateTimeUpDown.OnFormatStringChanged( ( string )e.OldValue, ( string )e.NewValue ); } protected virtual void OnFormatStringChanged( string oldValue, string newValue ) { FormatUpdated(); } #endregion //FormatString #region Kind public static readonly DependencyProperty KindProperty = DependencyProperty.Register( "Kind", typeof( DateTimeKind ), typeof( DateTimeUpDown ), new FrameworkPropertyMetadata( DateTimeKind.Unspecified, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnKindChanged ) ); public DateTimeKind Kind { get { return ( DateTimeKind )GetValue( KindProperty ); } set { SetValue( KindProperty, value ); } } private static void OnKindChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { DateTimeUpDown dateTimeUpDown = o as DateTimeUpDown; if( dateTimeUpDown != null ) dateTimeUpDown.OnKindChanged( ( DateTimeKind )e.OldValue, ( DateTimeKind )e.NewValue ); } protected virtual void OnKindChanged( DateTimeKind oldValue, DateTimeKind newValue ) { //Upate the value based on kind. (Postpone to EndInit if not yet initialized) if( !_setKindInternal && this.Value != null && this.IsInitialized ) { this.Value = this.ConvertToKind( this.Value.Value, newValue ); } } private void SetKindInternal( DateTimeKind kind ) { _setKindInternal = true; try { #if VS2008 // Warning : Binding could be lost this.Kind = kind; #else //We use SetCurrentValue to not erase the possible underlying //OneWay Binding. (This will also update correctly any //possible TwoWay bindings). this.SetCurrentValue( DateTimeUpDown.KindProperty, kind ); #endif } finally { _setKindInternal = false; } } #endregion //Kind #region ContextNow (Private) internal DateTime ContextNow { get { return DateTimeUtilities.GetContextNow( this.Kind ); } } #endregion #endregion //Properties #region Constructors static DateTimeUpDown() { DefaultStyleKeyProperty.OverrideMetadata( typeof( DateTimeUpDown ), new FrameworkPropertyMetadata( typeof( DateTimeUpDown ) ) ); MaximumProperty.OverrideMetadata( typeof( DateTimeUpDown ), new FrameworkPropertyMetadata( DateTime.MaxValue ) ); MinimumProperty.OverrideMetadata( typeof( DateTimeUpDown ), new FrameworkPropertyMetadata( DateTime.MinValue ) ); } public DateTimeUpDown() { } #endregion //Constructors #region Base Class Overrides public override bool CommitInput() { bool isSyncValid = this.SyncTextAndValueProperties( true, Text ); _lastValidDate = this.Value; return isSyncValid; } protected override void OnCultureInfoChanged( CultureInfo oldValue, CultureInfo newValue ) { FormatUpdated(); } protected override void OnIncrement() { if( this.IsCurrentValueValid() ) { if( Value.HasValue ) UpdateDateTime( 1 ); else Value = DefaultValue ?? this.ContextNow; } } protected override void OnDecrement() { if( this.IsCurrentValueValid() ) { if( Value.HasValue ) UpdateDateTime( -1 ); else Value = DefaultValue ?? this.ContextNow; } } protected override void OnTextChanged( string previousValue, string currentValue ) { if( !_processTextChanged ) return; base.OnTextChanged( previousValue, currentValue ); } protected override DateTime? ConvertTextToValue( string text ) { if( string.IsNullOrEmpty( text ) ) return null; DateTime result; this.TryParseDateTime( text, out result ); //Do not force "unspecified" to a time-zone specific //parsed text value. This would result in a lost of precision and //corrupt data. Let the value impose the Kind to the //DateTimePicker. if( this.Kind != DateTimeKind.Unspecified ) { //Keep the current kind (Local or Utc) //by imposing it to the parsed text value. // //Note: A parsed UTC text value may be // adjusted with a Local kind and time. result = this.ConvertToKind( result, this.Kind ); } if( this.ClipValueToMinMax ) { return this.GetClippedMinMaxValue( result ); } this.ValidateDefaultMinMax( result ); return result; } protected override string ConvertValueToText() { if( Value == null ) return string.Empty; return Value.Value.ToString( GetFormatString( Format ), CultureInfo ); } protected override void SetValidSpinDirection() { ValidSpinDirections validDirections = ValidSpinDirections.None; if( !IsReadOnly ) { if( this.IsLowerThan( this.Value, this.Maximum ) || !this.Value.HasValue || !this.Maximum.HasValue ) validDirections = validDirections | ValidSpinDirections.Increase; if( this.IsGreaterThan( this.Value, this.Minimum ) || !this.Value.HasValue || !this.Minimum.HasValue ) validDirections = validDirections | ValidSpinDirections.Decrease; } if( this.Spinner != null ) this.Spinner.ValidSpinDirection = validDirections; } protected override object OnCoerceValue( object newValue ) { //Since only changing the "kind" of a date //Ex. "2001-01-01 12:00 AM, Kind=Utc" to "2001-01-01 12:00 AM Kind=Local" //by setting the "Value" property won't trigger a property changed, //but will call this callback (coerce), we update the Kind here. DateTime? value = ( DateTime? )base.OnCoerceValue( newValue ); //Let the initialized determine the final "kind" value. if(value != null && this.IsInitialized) { //Update kind based on value kind this.SetKindInternal( value.Value.Kind ); } return value; } protected override void OnValueChanged( DateTime? oldValue, DateTime? newValue ) { //whenever the value changes we need to parse out the value into out DateTimeInfo segments so we can keep track of the individual pieces //but only if it is not null if( newValue != null ) ParseValueIntoDateTimeInfo(); base.OnValueChanged( oldValue, newValue ); if( !_isTextChangedFromUI ) { _lastValidDate = newValue; } } protected override void RaiseValueChangedEvent( DateTime? oldValue, DateTime? newValue ) { if( ( this.TemplatedParent is TimePicker ) && ( ( TimePicker )this.TemplatedParent ).TemplatedParent is DateTimePicker ) return; base.RaiseValueChangedEvent( oldValue, newValue ); } protected override bool IsCurrentValueValid() { DateTime result; if( string.IsNullOrEmpty( this.TextBox.Text ) ) return true; return this.TryParseDateTime( this.TextBox.Text, out result ); } protected override void OnInitialized( EventArgs e ) { base.OnInitialized( e ); if( this.Value != null ) { DateTimeKind valueKind = this.Value.Value.Kind; if( valueKind != this.Kind ) { //Conflit between "Kind" property and the "Value.Kind" value. //Priority to the one that is not "Unspecified". if( this.Kind == DateTimeKind.Unspecified ) { this.SetKindInternal( valueKind ); } else { this.Value = this.ConvertToKind( this.Value.Value, this.Kind ); } } } } #endregion //Base Class Overrides #region Methods public void SelectAll() { _fireSelectionChangedEvent = false; TextBox.SelectAll(); _fireSelectionChangedEvent = true; } private void FormatUpdated() { InitializeDateTimeInfoList(); if( Value != null ) ParseValueIntoDateTimeInfo(); // Update the Text representation of the value. _processTextChanged = false; this.SyncTextAndValueProperties( false, null ); _processTextChanged = true; } protected override void InitializeDateTimeInfoList() { _dateTimeInfoList.Clear(); _selectedDateTimeInfo = null; string format = GetFormatString( Format ); if( string.IsNullOrEmpty( format ) ) return; while( format.Length > 0 ) { int elementLength = GetElementLengthByFormat( format ); DateTimeInfo info = null; switch( format[ 0 ] ) { case '"': case '\'': { int closingQuotePosition = format.IndexOf( format[ 0 ], 1 ); info = new DateTimeInfo { IsReadOnly = true, Type = DateTimePart.Other, Length = 1, Content = format.Substring( 1, Math.Max( 1, closingQuotePosition - 1 ) ) }; elementLength = Math.Max( 1, closingQuotePosition + 1 ); break; } case 'D': case 'd': { string d = format.Substring( 0, elementLength ); if( elementLength == 1 ) d = "%" + d; if( elementLength > 2 ) info = new DateTimeInfo { IsReadOnly = true, Type = DateTimePart.DayName, Format = d }; else info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Day, Format = d }; break; } case 'F': case 'f': { string f = format.Substring( 0, elementLength ); if( elementLength == 1 ) f = "%" + f; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Millisecond, Format = f }; break; } case 'h': { string h = format.Substring( 0, elementLength ); if( elementLength == 1 ) h = "%" + h; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Hour12, Format = h }; break; } case 'H': { string H = format.Substring( 0, elementLength ); if( elementLength == 1 ) H = "%" + H; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Hour24, Format = H }; break; } case 'M': { string M = format.Substring( 0, elementLength ); if( elementLength == 1 ) M = "%" + M; if( elementLength >= 3 ) info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.MonthName, Format = M }; else info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Month, Format = M }; break; } case 'S': case 's': { string s = format.Substring( 0, elementLength ); if( elementLength == 1 ) s = "%" + s; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Second, Format = s }; break; } case 'T': case 't': { string t = format.Substring( 0, elementLength ); if( elementLength == 1 ) t = "%" + t; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.AmPmDesignator, Format = t }; break; } case 'Y': case 'y': { string y = format.Substring( 0, elementLength ); if( elementLength == 1 ) y = "%" + y; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Year, Format = y }; break; } case '\\': { if( format.Length >= 2 ) { info = new DateTimeInfo { IsReadOnly = true, Content = format.Substring( 1, 1 ), Length = 1, Type = DateTimePart.Other }; elementLength = 2; } break; } case 'g': { string g = format.Substring( 0, elementLength ); if( elementLength == 1 ) g = "%" + g; info = new DateTimeInfo { IsReadOnly = true, Type = DateTimePart.Period, Format = format.Substring( 0, elementLength ) }; break; } case 'm': { string m = format.Substring( 0, elementLength ); if( elementLength == 1 ) m = "%" + m; info = new DateTimeInfo { IsReadOnly = false, Type = DateTimePart.Minute, Format = m }; break; } case 'z': { string z = format.Substring( 0, elementLength ); if( elementLength == 1 ) z = "%" + z; info = new DateTimeInfo { IsReadOnly = true, Type = DateTimePart.TimeZone, Format = z }; break; } default: { elementLength = 1; info = new DateTimeInfo { IsReadOnly = true, Length = 1, Content = format[ 0 ].ToString(), Type = DateTimePart.Other }; break; } } _dateTimeInfoList.Add( info ); format = format.Substring( elementLength ); } } protected override bool IsLowerThan( DateTime? value1, DateTime? value2 ) { if( value1 == null || value2 == null ) return false; return ( value1.Value < value2.Value ); } protected override bool IsGreaterThan( DateTime? value1, DateTime? value2 ) { if( value1 == null || value2 == null ) return false; return ( value1.Value > value2.Value ); } private static int GetElementLengthByFormat( string format ) { for( int i = 1; i < format.Length; i++ ) { if( String.Compare( format[ i ].ToString(), format[ 0 ].ToString(), false ) != 0 ) { return i; } } return format.Length; } private void ParseValueIntoDateTimeInfo() { string text = string.Empty; _dateTimeInfoList.ForEach( info => { if( info.Format == null ) { info.StartPosition = text.Length; info.Length = info.Content.Length; text += info.Content; } else { DateTime date = Value.Value; info.StartPosition = text.Length; info.Content = date.ToString( info.Format, CultureInfo.DateTimeFormat ); info.Length = info.Content.Length; text += info.Content; } } ); } internal string GetFormatString( DateTimeFormat dateTimeFormat ) { switch( dateTimeFormat ) { case DateTimeFormat.ShortDate: return CultureInfo.DateTimeFormat.ShortDatePattern; case DateTimeFormat.LongDate: return CultureInfo.DateTimeFormat.LongDatePattern; case DateTimeFormat.ShortTime: return CultureInfo.DateTimeFormat.ShortTimePattern; case DateTimeFormat.LongTime: return CultureInfo.DateTimeFormat.LongTimePattern; case DateTimeFormat.FullDateTime: return CultureInfo.DateTimeFormat.FullDateTimePattern; case DateTimeFormat.MonthDay: return CultureInfo.DateTimeFormat.MonthDayPattern; case DateTimeFormat.RFC1123: return CultureInfo.DateTimeFormat.RFC1123Pattern; case DateTimeFormat.SortableDateTime: return CultureInfo.DateTimeFormat.SortableDateTimePattern; case DateTimeFormat.UniversalSortableDateTime: return CultureInfo.DateTimeFormat.UniversalSortableDateTimePattern; case DateTimeFormat.YearMonth: return CultureInfo.DateTimeFormat.YearMonthPattern; case DateTimeFormat.Custom: { switch( this.FormatString ) { case "d": return CultureInfo.DateTimeFormat.ShortDatePattern; case "t": return CultureInfo.DateTimeFormat.ShortTimePattern; case "T": return CultureInfo.DateTimeFormat.LongTimePattern; case "D": return CultureInfo.DateTimeFormat.LongDatePattern; case "f": return CultureInfo.DateTimeFormat.LongDatePattern + " " + CultureInfo.DateTimeFormat.ShortTimePattern; case "F": return CultureInfo.DateTimeFormat.FullDateTimePattern; case "g": return CultureInfo.DateTimeFormat.ShortDatePattern + " " + CultureInfo.DateTimeFormat.ShortTimePattern; case "G": return CultureInfo.DateTimeFormat.ShortDatePattern + " " + CultureInfo.DateTimeFormat.LongTimePattern; case "m": return CultureInfo.DateTimeFormat.MonthDayPattern; case "y": return CultureInfo.DateTimeFormat.YearMonthPattern; case "r": return CultureInfo.DateTimeFormat.RFC1123Pattern; case "s": return CultureInfo.DateTimeFormat.SortableDateTimePattern; case "u": return CultureInfo.DateTimeFormat.UniversalSortableDateTimePattern; default: return FormatString; } } default: throw new ArgumentException( "Not a supported format" ); } } private void UpdateDateTime( int value ) { _fireSelectionChangedEvent = false; DateTimeInfo info = _selectedDateTimeInfo; //this only occurs when the user manually type in a value for the Value Property if( info == null ) info = _dateTimeInfoList[ 0 ]; DateTime? result = null; try { switch( info.Type ) { case DateTimePart.Year: { result = ( ( DateTime )Value ).AddYears( value ); break; } case DateTimePart.Month: case DateTimePart.MonthName: { result = ( ( DateTime )Value ).AddMonths( value ); break; } case DateTimePart.Day: case DateTimePart.DayName: { result = ( ( DateTime )Value ).AddDays( value ); break; } case DateTimePart.Hour12: case DateTimePart.Hour24: { result = ( ( DateTime )Value ).AddHours( value ); break; } case DateTimePart.Minute: { result = ( ( DateTime )Value ).AddMinutes( value ); break; } case DateTimePart.Second: { result = ( ( DateTime )Value ).AddSeconds( value ); break; } case DateTimePart.Millisecond: { result = ( ( DateTime )Value ).AddMilliseconds( value ); break; } case DateTimePart.AmPmDesignator: { result = ( ( DateTime )Value ).AddHours( value * 12 ); break; } default: { break; } } } catch { //this can occur if the date/time = 1/1/0001 12:00:00 AM which is the smallest date allowed. //I could write code that would validate the date each and everytime but I think that it would be more //efficient if I just handle the edge case and allow an exeption to occur and swallow it instead. } this.Value = this.CoerceValueMinMax( result ); //we loose our selection when the Value is set so we need to reselect it without firing the selection changed event TextBox.Select( info.StartPosition, info.Length ); _fireSelectionChangedEvent = true; } private bool TryParseDateTime( string text, out DateTime result ) { bool isValid = false; result = this.ContextNow; DateTime current = this.ContextNow; try { current = (this.Value.HasValue) ? this.Value.Value : DateTime.Parse( this.ContextNow.ToString(), this.CultureInfo.DateTimeFormat ); isValid = DateTimeParser.TryParse( text, this.GetFormatString( Format ), current, this.CultureInfo, out result ); } catch( FormatException ) { isValid = false; } if( !isValid ) { isValid = DateTime.TryParseExact( text, this.GetFormatString( this.Format ), this.CultureInfo, DateTimeStyles.None, out result ); } if( !isValid ) { result = ( _lastValidDate != null ) ? _lastValidDate.Value : current; } return isValid; } private DateTime ConvertToKind( DateTime dateTime, DateTimeKind kind ) { //Same kind, just return same value. if( kind == dateTime.Kind ) return dateTime; //"ToLocalTime()" from an unspecified will assume // That the time was originaly Utc and affect the datetime value. // Just "Force" the "Kind" instead. if( dateTime.Kind == DateTimeKind.Unspecified || kind == DateTimeKind.Unspecified ) return DateTime.SpecifyKind( dateTime, kind ); return ( kind == DateTimeKind.Local ) ? dateTime.ToLocalTime() : dateTime.ToUniversalTime(); } #endregion //Methods } }