All the controls missing in WPF. Over 1 million downloads.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

645 lines
22 KiB

/*************************************************************************************
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.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using Xceed.Wpf.Toolkit.Primitives;
namespace Xceed.Wpf.Toolkit
{
public class TimeSpanUpDown : DateTimeUpDownBase<TimeSpan?>
{
#region Constructors
static TimeSpanUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata( typeof( TimeSpanUpDown ), new FrameworkPropertyMetadata( typeof( TimeSpanUpDown ) ) );
MaximumProperty.OverrideMetadata( typeof( TimeSpanUpDown ), new FrameworkPropertyMetadata( TimeSpan.MaxValue ) );
MinimumProperty.OverrideMetadata( typeof( TimeSpanUpDown ), new FrameworkPropertyMetadata( TimeSpan.MinValue ) );
DefaultValueProperty.OverrideMetadata( typeof( TimeSpanUpDown ), new FrameworkPropertyMetadata( TimeSpan.Zero ) );
}
public TimeSpanUpDown()
{
DataObject.AddPastingHandler( this, this.OnPasting );
}
#endregion //Constructors
#region Properties
#region FractionalSecondsDigitsCount
public static readonly DependencyProperty FractionalSecondsDigitsCountProperty = DependencyProperty.Register( "FractionalSecondsDigitsCount", typeof( int ), typeof( TimeSpanUpDown ), new UIPropertyMetadata( 0, OnFractionalSecondsDigitsCountChanged, OnCoerceFractionalSecondsDigitsCount ) );
public int FractionalSecondsDigitsCount
{
get
{
return ( int )GetValue( FractionalSecondsDigitsCountProperty );
}
set
{
SetValue( FractionalSecondsDigitsCountProperty, value );
}
}
private static object OnCoerceFractionalSecondsDigitsCount( DependencyObject o, object value )
{
TimeSpanUpDown timeSpanUpDown = o as TimeSpanUpDown;
if( timeSpanUpDown != null )
{
int digitsCount = (int)value;
if( digitsCount < 0 || digitsCount > 3 )
throw new ArgumentException( "Fractional seconds digits count must be between 0 and 3." );
}
return value;
}
private static void OnFractionalSecondsDigitsCountChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
{
TimeSpanUpDown timeSpanUpDown = o as TimeSpanUpDown;
if( timeSpanUpDown != null )
timeSpanUpDown.OnFractionalSecondsDigitsCountChanged( ( int )e.OldValue, ( int )e.NewValue );
}
protected virtual void OnFractionalSecondsDigitsCountChanged( int oldValue, int newValue )
{
this.UpdateValue();
}
#endregion //FractionalSecondsDigitsCount
#region ShowDays
public static readonly DependencyProperty ShowDaysProperty = DependencyProperty.Register( "ShowDays", typeof( bool ), typeof( TimeSpanUpDown ), new UIPropertyMetadata( true, OnShowDaysChanged ) );
public bool ShowDays
{
get
{
return (bool)GetValue( ShowDaysProperty );
}
set
{
SetValue( ShowDaysProperty, value );
}
}
private static void OnShowDaysChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
{
var timeSpanUpDown = o as TimeSpanUpDown;
if( timeSpanUpDown != null )
timeSpanUpDown.OnShowDaysChanged( (bool)e.OldValue, (bool)e.NewValue );
}
protected virtual void OnShowDaysChanged( bool oldValue, bool newValue )
{
this.UpdateValue();
}
#endregion //ShowDays
#region ShowSeconds
public static readonly DependencyProperty ShowSecondsProperty = DependencyProperty.Register( "ShowSeconds", typeof( bool ), typeof( TimeSpanUpDown ), new UIPropertyMetadata( true, OnShowSecondsChanged ) );
public bool ShowSeconds
{
get
{
return ( bool )GetValue( ShowSecondsProperty );
}
set
{
SetValue( ShowSecondsProperty, value );
}
}
private static void OnShowSecondsChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
{
TimeSpanUpDown timeSpanUpDown = o as TimeSpanUpDown;
if( timeSpanUpDown != null )
timeSpanUpDown.OnShowSecondsChanged( ( bool )e.OldValue, ( bool )e.NewValue );
}
protected virtual void OnShowSecondsChanged( bool oldValue, bool newValue )
{
this.UpdateValue();
}
#endregion //ShowSeconds
#endregion
#region BaseClass Overrides
protected override void OnCultureInfoChanged( CultureInfo oldValue, CultureInfo newValue )
{
var value = this.UpdateValueOnEnterKey
? (this.TextBox != null) ? this.ConvertTextToValue( this.TextBox.Text ) : null
: this.Value;
this.InitializeDateTimeInfoList( value );
}
protected override void SetValidSpinDirection()
{
ValidSpinDirections validDirections = ValidSpinDirections.None;
if( !this.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 void OnIncrement()
{
this.Increment( this.Step );
}
protected override void OnDecrement()
{
this.Increment( -this.Step );
}
protected override string ConvertValueToText()
{
if( this.Value == null )
return string.Empty;
return this.ParseValueIntoTimeSpanInfo( this.Value, true );
}
protected override TimeSpan? ConvertTextToValue( string text )
{
if( string.IsNullOrEmpty( text ) )
return null;
var timeSpan = TimeSpan.MinValue;
if( this.ShowDays )
{
timeSpan = TimeSpan.Parse( text );
}
else
{
var separators = text.Where( x => x == ':' || x == '.' ).ToList();
var values = text.Split( new char[] { ':', '.' } );
if( ( values.Count() >= 2 ) && !values.Any( x => string.IsNullOrEmpty( x ) ) )
{
bool haveMS = ( separators.Count() > 1 ) && ( separators.Last() == '.' );
timeSpan = new TimeSpan( 0, //Days
int.Parse( values[ 0 ].Replace("-", "") ), //Hours
int.Parse( values[ 1 ].Replace( "-", "" ) ), //Minutes
this.ShowSeconds ? int.Parse( values[ 2 ].Replace( "-", "" ) ) : 0, //Seconds
haveMS ? int.Parse( values.Last().Replace( "-", "" ) ) : 0 ); //Milliseconds
if( text.StartsWith( "-" ) )
{
timeSpan = timeSpan.Negate();
}
}
}
if( this.ClipValueToMinMax )
{
return this.GetClippedMinMaxValue( timeSpan );
}
this.ValidateDefaultMinMax( timeSpan );
return timeSpan;
}
protected override void OnPreviewTextInput( TextCompositionEventArgs e )
{
e.Handled = !this.IsNumber( e.Text );
base.OnPreviewTextInput( e );
}
protected override void OnPreviewKeyDown( KeyEventArgs e )
{
if( e.Key == Key.Space )
{
e.Handled = true;
}
base.OnPreviewKeyDown( e );
}
protected override void OnTextChanged( string previousValue, string currentValue )
{
if( !_processTextChanged )
return;
if( String.IsNullOrEmpty( currentValue ) )
{
if( !this.UpdateValueOnEnterKey )
{
this.Value = null;
}
return;
}
TimeSpan current = this.Value.HasValue ? this.Value.Value : new TimeSpan();
TimeSpan result;
var success = TimeSpan.TryParse( currentValue, out result );
if( !success )
{
// Validate when more than 60 seconds (or more than 60 minutes, or more than 24 hours) are entered.
var separators = currentValue.Where( x => x == ':' || x == '.' ).ToList();
var values = currentValue.Split( new char[] { ':', '.' } );
if( ( values.Count() >= 2 ) && !values.Any( x => string.IsNullOrEmpty( x ) ) )
{
bool haveDays = separators.First() == '.';
bool haveMS = ( separators.Count() > 1 ) && ( separators.Last() == '.' );
result = new TimeSpan( haveDays ? int.Parse( values[ 0 ] ) : 0, //Days
haveDays ? int.Parse( values[ 1 ] ) : int.Parse( values[ 0 ] ), //Hours
haveDays ? int.Parse( values[ 2 ] ) : int.Parse( values[ 1 ] ), //Minutes
( haveDays && this.ShowSeconds ) ? int.Parse( values[ 3 ] ) : this.ShowSeconds ? int.Parse( values[ 2 ] ) : 0, //Seconds
haveMS ? int.Parse( values.Last() ) : 0 ); //Milliseconds
}
}
currentValue = result.ToString();
// When text is typed, if UpdateValueOnEnterKey is true,
// Sync Value on Text only when Enter Key is pressed.
if( ( _isTextChangedFromUI && !this.UpdateValueOnEnterKey )
|| !_isTextChangedFromUI )
{
this.SyncTextAndValueProperties( true, currentValue );
}
}
protected override void OnValueChanged( TimeSpan? oldValue, TimeSpan? 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 )
{
var value = this.UpdateValueOnEnterKey
? (this.TextBox != null) ? this.ConvertTextToValue( this.TextBox.Text ) : null
: this.Value;
this.InitializeDateTimeInfoList( value );
}
base.OnValueChanged( oldValue, newValue );
}
protected override void PerformMouseSelection()
{
var value = this.UpdateValueOnEnterKey
? (this.TextBox != null) ? this.ConvertTextToValue( this.TextBox.Text ) : null
: this.Value;
this.InitializeDateTimeInfoList( value );
base.PerformMouseSelection();
}
protected override void InitializeDateTimeInfoList( TimeSpan? value )
{
var lastDayInfo = _dateTimeInfoList.FirstOrDefault( x => x.Type == DateTimePart.Day );
bool hasDay = lastDayInfo != null;
var negInfo = _dateTimeInfoList.FirstOrDefault( x => x.Type == DateTimePart.Other );
bool hasNegative = (negInfo != null) && (negInfo.Content == "-");
_dateTimeInfoList.Clear();
if( value.HasValue && value.Value.TotalMilliseconds < 0 )
{
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Other, Length = 1, Content = "-", IsReadOnly = true } );
// Negative has been added, move TextBox.Selection to keep it on current DateTimeInfo
if( !hasNegative && (this.TextBox != null) )
{
_fireSelectionChangedEvent = false;
this.TextBox.SelectionStart++;
_fireSelectionChangedEvent = true;
}
}
if( this.ShowDays )
{
if( value.HasValue && value.Value.Days != 0 )
{
int dayLength = Math.Abs( value.Value.Days ).ToString().Length;
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Day, Length = dayLength, Format = "dd" } );
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Other, Length = 1, Content = ".", IsReadOnly = true } );
if( this.TextBox != null )
{
//number of digits for days has changed when selection is not on date part, move TextBox.Selection to keep it on current DateTimeInfo
if( hasDay && ( dayLength != lastDayInfo.Length ) && ( _selectedDateTimeInfo.Type != DateTimePart.Day ) )
{
_fireSelectionChangedEvent = false;
this.TextBox.SelectionStart = Math.Max( 0, this.TextBox.SelectionStart + ( dayLength - lastDayInfo.Length ) );
_fireSelectionChangedEvent = true;
}
// Day has been added, move TextBox.Selection to keep it on current DateTimeInfo
else if( !hasDay )
{
_fireSelectionChangedEvent = false;
this.TextBox.SelectionStart += ( dayLength + 1 );
_fireSelectionChangedEvent = true;
}
}
}
// Day has been removed, move TextBox.Selection to keep it on current DateTimeInfo
else if( hasDay )
{
_fireSelectionChangedEvent = false;
this.TextBox.SelectionStart = Math.Max( hasNegative ? 1 : 0, this.TextBox.SelectionStart - ( lastDayInfo.Length + 1 ) );
_fireSelectionChangedEvent = true;
}
}
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Hour24, Length = 2, Format = "hh" } );
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Other, Length = 1, Content = ":", IsReadOnly = true } );
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Minute, Length = 2, Format = "mm" } );
if( this.ShowSeconds )
{
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Other, Length = 1, Content = ":", IsReadOnly = true } );
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Second, Length = 2, Format = "ss" } );
}
if( this.FractionalSecondsDigitsCount > 0 )
{
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Other, Length = 1, Content = ".", IsReadOnly = true } );
string fraction = new string( 'f', this.FractionalSecondsDigitsCount );
//If the "f" custom format specifier is used alone, specify "%f" so that it is not misinterpreted as a standard format string.
if( fraction.Length == 1 )
{
fraction = "%" + fraction;
}
_dateTimeInfoList.Add( new DateTimeInfo() { Type = DateTimePart.Millisecond, Length = this.FractionalSecondsDigitsCount, Format = fraction } );
}
if( value.HasValue )
{
this.ParseValueIntoTimeSpanInfo( value, true );
}
}
protected override bool IsLowerThan( TimeSpan? value1, TimeSpan? value2 )
{
if( value1 == null || value2 == null )
return false;
return (value1.Value < value2.Value);
}
protected override bool IsGreaterThan( TimeSpan? value1, TimeSpan? value2 )
{
if( value1 == null || value2 == null )
return false;
return (value1.Value > value2.Value);
}
#endregion
#region Methods
private string ParseValueIntoTimeSpanInfo( TimeSpan? value, bool modifyInfo )
{
string text = string.Empty;
_dateTimeInfoList.ForEach( info =>
{
if( info.Format == null )
{
if( modifyInfo )
{
info.StartPosition = text.Length;
info.Length = info.Content.Length;
}
text += info.Content;
}
else
{
TimeSpan span = TimeSpan.Parse( value.ToString() );
if( modifyInfo )
{
info.StartPosition = text.Length;
}
var content = "";
#if VS2008
switch (info.Format)
{
case "hh":
// Display days and hours or totalHours
content = ( !this.ShowDays && ( span.Days != 0 ) && ( info.Format == "hh" ) )
? Math.Truncate( Math.Abs( span.TotalHours ) ).ToString("00")
: span.Hours.ToString("00");
break;
case "mm":
content = span.Minutes.ToString("00");
break;
case "ss":
content = span.Seconds.ToString("00");
break;
case "dd":
content = span.Days.ToString();
break;
case "%f":
content = (span.Milliseconds / 100).ToString();
break;
case "ff":
content = (span.Milliseconds / 10).ToString();
break;
case "fff":
content = span.Milliseconds.ToString();
break;
default:
throw new InvalidOperationException("Wrong TimeSpan format");
}
#else
// Display days and hours or totalHours
content = ( !this.ShowDays && ( span.Days != 0 ) && ( info.Format == "hh" ) )
? Math.Truncate( Math.Abs( span.TotalHours ) ).ToString()
: span.ToString( info.Format, this.CultureInfo.DateTimeFormat );
#endif
if( modifyInfo )
{
if( info.Format == "dd" )
{
content = Convert.ToInt32( content ).ToString();
}
info.Content = content;
info.Length = info.Content.Length;
}
text += content;
}
} );
return text;
}
private TimeSpan? UpdateTimeSpan( TimeSpan? currentValue, int value )
{
DateTimeInfo info = _selectedDateTimeInfo;
//this only occurs when the user manually type in a value for the Value Property
if( info == null )
{
info = (this.CurrentDateTimePart != DateTimePart.Other)
? this.GetDateTimeInfo( this.CurrentDateTimePart )
: (_dateTimeInfoList[ 0 ].Content != "-") ? _dateTimeInfoList[ 0 ] : _dateTimeInfoList[ 1 ]; //Skip negative sign
}
TimeSpan? result = null;
try
{
switch( info.Type )
{
case DateTimePart.Day:
result = ( ( TimeSpan )currentValue).Add( new TimeSpan( value, 0, 0, 0, 0 ) );
break;
case DateTimePart.Hour24:
result = ( ( TimeSpan )currentValue).Add( new TimeSpan( 0, value, 0, 0, 0 ) );
break;
case DateTimePart.Minute:
result = ( ( TimeSpan )currentValue).Add( new TimeSpan( 0, 0, value, 0, 0 ) );
break;
case DateTimePart.Second:
result = ( ( TimeSpan )currentValue).Add( new TimeSpan( 0, 0, 0, value, 0 ) );
break;
case DateTimePart.Millisecond:
switch( this.FractionalSecondsDigitsCount )
{
case 1:
value = value * 100;
break;
case 2:
value = value * 10;
break;
default:
value = value * 1;
break;
}
result = ( ( TimeSpan )currentValue).Add( new TimeSpan( 0, 0, 0, 0, value ) );
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.
}
result = ( ( result != null ) && result.HasValue )
? result.Value
: result;
result = this.CoerceValueMinMax( result );
return result;
}
private void Increment( int step )
{
// 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 newValue = currentValue.HasValue
? this.UpdateTimeSpan( currentValue, step )
: this.DefaultValue ?? TimeSpan.Zero;
if( newValue != null )
{
this.InitializeDateTimeInfoList( newValue );
var selectionStart = this.TextBox.SelectionStart;
var selectionLength = this.TextBox.SelectionLength;
this.TextBox.Text = this.ParseValueIntoTimeSpanInfo( newValue, false );
this.TextBox.Select( selectionStart, selectionLength );
}
}
else
{
if( this.Value.HasValue )
{
var newValue = this.UpdateTimeSpan( this.Value, step );
if( newValue != null )
{
this.InitializeDateTimeInfoList( newValue );
var selectionStart = this.TextBox.SelectionStart;
var selectionLength = this.TextBox.SelectionLength;
this.Value = newValue;
this.TextBox.Select( selectionStart, selectionLength );
}
}
else
{
this.Value = this.DefaultValue ?? TimeSpan.Zero;
}
}
}
private bool IsNumber( string str )
{
foreach( char c in str )
{
if( !char.IsNumber( c ) )
return false;
}
return true;
}
private void UpdateValue()
{
var value = this.UpdateValueOnEnterKey
? ( this.TextBox != null ) ? this.ConvertTextToValue( this.TextBox.Text ) : null
: this.Value;
this.InitializeDateTimeInfoList( value );
this.SyncTextAndValueProperties( false, this.Text );
}
#endregion
#region Event Handlers
private void OnPasting( object sender, DataObjectPastingEventArgs e )
{
if( e.DataObject.GetDataPresent( typeof( string ) ) )
{
// Allow pasting only TimeSpan values
var pasteText = e.DataObject.GetData( typeof( string ) ) as string;
TimeSpan result;
var success = TimeSpan.TryParse( pasteText, out result );
if( !success )
{
e.CancelCommand();
}
}
}
#endregion
}
}