Click here to Skip to main content
15,880,956 members
Articles / Desktop Programming / WPF

A framework for comprehensive validation of user input

Rate me:
Please Sign up or sign in to vote.
4.70/5 (8 votes)
19 Jun 2012CPOL28 min read 31.1K   522   17  
Validation of input made as easy as possible for Windows.Forms, WPF, console-applications or any other purposes
// Copyright (c) 2010 - 2012, Andreas Ganzer. All Rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using Ganzer.Validation;

namespace Ganzer.Wpf.Validation
{
	//############################################################################
	/// <summary>
	/// The DatePickerLink class defines a link for <see cref="DatePicker"/> controls.
	/// </summary>
	/// 
	/// <seealso cref="Validator"/>
	/// 
	public class DatePickerLink : ValidatorLink
	{
		#region types

		/// <summary>
		/// The TextInfo class encapsulates text settings for pending changings.
		/// </summary>
		/// 
		protected class TextInfo
		{
			#region properties

			/// <summary>
			/// Gets the start index of the selection.
			/// </summary>
			/// 
			/// <returns>The start index.</returns>
			/// 
			public int SelectionStart
			{
				get;
				private set;
			}

			/// <summary>
			/// Gets the length of the selection.
			/// </summary>
			/// 
			/// <returns>The length index.</returns>
			/// 
			public int SelectionLength
			{
				get;
				private set;
			}

			/// <summary>
			/// Gets the text to set.
			/// </summary>
			/// 
			/// <returns>The text to set.</returns>
			/// 
			public string Text
			{
				get;
				private set;
			}

			#endregion

			#region initialization

			/// <summary>
			/// Initializes this object with the specifeid arguments.
			/// </summary>
			/// 
			/// <param name="text">The text to set.</param>
			/// <param name="selStart">The start of the selection.</param>
			/// <param name="selLength">The length of the selection.</param>
			/// 
			public TextInfo( string text, int selStart, int selLength )
			{
				Text = text ?? string.Empty;

				if( selStart < 0 )
					SelectionStart = 0;
				else if( selStart > Text.Length )
					SelectionStart = Text.Length;
				else
					SelectionStart = selStart;

				if( selLength < 0 )
					SelectionLength = 0;
				else if( SelectionStart + selLength > Text.Length )
					SelectionLength = Text.Length - SelectionStart;
				else
					SelectionLength = selLength;
			}

			#endregion
		}

		#endregion

		#region fields

		private bool __preventTextChangeValidation;

		#endregion

		#region properties

		/// <summary>
		/// Gets or sets the line editor control that is linked with the validator.
		/// </summary>
		/// 
		/// <returns><see cref="Control">Control</see> as <see cref="DatePicker"/>.
		///   </returns>
		/// 
		/// <remarks>
		/// If this property is <c>null</c> <see cref="ValidatorLink.Validate()">Validate()</see>
		/// does always return <c>true.</c>
		/// </remarks>
		/// 
		/// <seealso cref="Control"/>
		/// <seealso cref="ValidatorLink.Validator">Validator</seealso>
		///
		public DatePicker DatePicker
		{
			get
			{
				Debug.Assert(Control == null || Control is DatePicker);
				return Control as DatePicker;
			}
			set
			{
				this.Control = value;
			}
		}

		/// <summary>
		/// Gets or sets the control that is linked with the validator.
		/// </summary>
		/// 
		/// <returns>The control to validate.</returns>
		/// 
		/// <exception cref="ArgumentException">The value to set is not of type
		///   <see cref="DatePicker"/> and is not <c>null</c>.</exception>
		///   
		/// <remarks>
		/// If this property is <c>null</c> <see cref="ValidatorLink.Validate()">Validate()</see>
		/// does always return <c>true.</c>
		/// </remarks>
		/// 
		/// <seealso cref="ValidatorLink.Validator">Validator</seealso>
		///
		public override IInputElement Control
		{
			get
			{
				return base.Control;
			}
			set
			{
				Debug.Assert(value == null || value is DatePicker);

				if( value != null && !(value is DatePicker) )
					throw new ArgumentException("The specified value must be of type DatePicker.", "value");

				if( value == base.Control )
					return;

				if( DatePicker != null )
					DatePicker.Loaded -= Control_Loaded;

				base.Control = value;

				if( DatePicker != null )
					DatePicker.Loaded += Control_Loaded;
			}
		}

		/// <summary>
		/// Gets or sets the pending text info.
		/// </summary>
		/// 
		/// <returns>The pending info or <c>null</c> if no info is pending.
		///   </returns>
		///   
		/// <remarks>
		/// This must be queried and resetted wihtin the text change event of
		/// the control.
		/// </remarks>
		/// 
		protected TextInfo PendingTextInfo
		{
			get;
			set;
		}

		/// <summary>
		/// This is used by <see cref="ValidatorLink.Text"/> and returns the text to validate.
		/// </summary>
		/// 
		/// <returns>The text content of <see cref="Control"/>.</returns>
		/// 
		protected override string ControlText
		{
			get
			{
				if( PickerTextBox == null )
					return DatePicker.Text;

				int selStart = PickerTextBox.SelectionStart;
				int selLength = PickerTextBox.SelectionLength;

				PickerTextBox.SelectAll();

				string text = PickerTextBox.SelectedText;

				PickerTextBox.Select(selStart, selLength);

				return text;
			}
		}

		/// <summary>
		/// This is used by <see cref="ValidatorLink.BindingExpression"/> and returns
		/// the binding expression that is used to bind the text of the control.
		/// </summary>
		/// 
		/// <returns>The used expression or <c>null</c> if the text is not bound.</returns>
		/// 
		protected override BindingExpressionBase CurrentBindingExpression
		{
			get
			{
				return BindingOperations.GetBindingExpressionBase(DatePicker, DatePicker.TextProperty);
			}
		}

		/// <summary>
		/// This is used by <see cref="ValidatorLink.Binding"/> and returns the binding
		/// that is used to bind the text of the control.
		/// </summary>
		/// 
		/// <returns>The used binding or <c>null</c> if the text is not bound.</returns>
		/// 
		protected override BindingBase CurrentBinding
		{
			get
			{
				return BindingOperations.GetBindingBase(DatePicker, DatePicker.TextProperty);
			}
		}

		private TextBox _textBox;

		/// <summary>
		/// Gets the text box of the linked control.
		/// </summary>
		/// 
		/// <returns>The text box of the linked control or <c>null</c> if the control
		///   is not loaded yet.</returns>
		/// 
		private TextBox PickerTextBox
		{
			get
			{
				if( _textBox == null )
					_textBox = DatePicker.Template.FindName("PART_TextBox", DatePicker) as TextBox;

				return _textBox;
			}
		}

		#endregion

		#region ctor/dtor

		/// <summary>
		/// Initializes this object with default values.
		/// </summary>
		/// 
		public DatePickerLink()
		{
		}

		/// <summary>
		/// Links the specifed validator with the specifed control.
		/// </summary>
		/// 
		/// <param name="control">The control to link with the given validator.</param>
		/// <param name="validator">The validator to link with the given control.</param>
		/// <param name="options">The options to set.</param>
		/// 
		public DatePickerLink( DatePicker control, Validator validator, LinkOptions options )
			: base(control, validator, (IInputElement)null, options)
		{
			if( DatePicker != null )
				DatePicker.Loaded += Control_Loaded;
		}

		/// <summary>
		/// Links the specifed validator with the specifed control.
		/// </summary>
		/// 
		/// <param name="control">The control to link with the given validator.</param>
		/// <param name="validator">The validator to link with the given control.</param>
		/// <param name="ignored">The control to ignore. This is used only if the
		///   validator contains the option <see cref="LinkOptions.ForceValid"/>.
		///   In this case the controls text is not validated if the input focus is set
		///   to the control given in <c>ignored</c>.</param>
		/// <param name="options">The options to set.</param>
		/// 
		public DatePickerLink( DatePicker control, Validator validator, IInputElement ignored, LinkOptions options )
			: base(control, validator, ignored, options)
		{
			if( DatePicker != null )
				DatePicker.Loaded += Control_Loaded;
		}

		/// <summary>
		/// Links the specifed validator with the specifed control.
		/// </summary>
		/// 
		/// <param name="control">The control to link with the given validator.</param>
		/// <param name="validator">The validator to link with the given control.</param>
		/// <param name="ignored">The controls to ignore. This is used only if the
		///   validator contains the option <see cref="LinkOptions.ForceValid"/>.
		///   In this case the controls text is not validated if the input focus is set
		///   to one of the control given in <c>ignored</c>.</param>
		/// <param name="options">The options to set.</param>
		/// 
		public DatePickerLink( DatePicker control, Validator validator, IEnumerable<IInputElement> ignored, LinkOptions options )
			: base(control, validator, ignored, options)
		{
			if( DatePicker != null )
				DatePicker.Loaded += Control_Loaded;
		}

		#endregion

		#region methods

		/// <summary>
		/// Validates the text of the control.
		/// </summary>
		/// 
		/// <param name="x">This ist set to <c>null</c> if the text is valid. Otherwise
		///   this ist set to an instance of a <see cref="ValidatorException"/> that
		///   describes the error.</param>
		/// 
		/// <returns>Return <c>true</c> if the text of the control is valid,
		///   otherwise <c>false</c> is returned.</returns>
		/// 
		protected override bool Validate( out ValidatorException x )
		{
			DateTimeValidator validator = Validator as DateTimeValidator;
			string text = ControlText;
			DateTime time;

			if( validator == null || !DateTime.TryParse(text, out time) )
				return Validator.Validate(text, CultureResolved, out x);

			switch( validator.DateTimeType )
			{
				case DateTimeType.Date:
					return Validator.Validate(time.ToString("d"), CultureResolved, out x);

				case DateTimeType.ShortTime:
					return Validator.Validate(time.ToString("t"), CultureResolved, out x);

				case DateTimeType.LongTime:
					return Validator.Validate(time.ToString("T"), CultureResolved, out x);

				default:
					return Validator.Validate(text, CultureResolved, out x);
			}
		}

		/// <summary>
		/// Selects the text of the control if possible.
		/// </summary>
		/// 
		protected override void SelectAllText()
		{
			if( PickerTextBox != null )
				PickerTextBox.SelectAll();
		}

		/// <summary>
		/// Formats the text for displaying by calling <see cref="Validator.FormatText(string, TextFormat)"/>.
		/// </summary>
		/// 
		/// <param name="frmt">How to format.</param>
		/// 
		protected override void FormatText( TextFormat frmt )
		{
			__preventTextChangeValidation = true;

			try
			{
				DatePicker.Text = Validator.FormatText(ControlText, frmt, CultureResolved);
			}
			finally
			{
				__preventTextChangeValidation = false;
			}
		}

		/// <summary>
		/// Handles the input of the specified text.
		/// </summary>
		/// 
		/// <param name="input">The text to insert into the control.</param>
		/// 
		/// <returns><c>true</c> when the input was valid; otherwise, <c>false</c> is
		///   returned.</returns>
		///   
		private bool HandleInput( string input )
		{
			Debug.Assert(PickerTextBox != null);

			StringBuilder text = new StringBuilder(ControlText);
			int selstart = PickerTextBox.SelectionStart;
			int sellength = PickerTextBox.SelectionLength;
			bool appending = selstart >= text.Length || selstart + sellength >= text.Length;
			int maxlen = PickerTextBox.MaxLength;

			if( maxlen <= 0 || text.Length < maxlen || sellength > 0 )
			{
				if( sellength > 0 )
					text.Remove(selstart, sellength);

				if( appending )
					text.Append(input);
				else
					text.Insert(selstart, input);

				if( Validator.IsValidInput(text, appending || HasOption(LinkOptions.FillAlways), CultureResolved) )
				{
					string txt = text.ToString();

					if( maxlen > 0 && maxlen < txt.Length )
						txt = txt.Substring(0, maxlen);

					if( HasOption(LinkOptions.SelectFilled) )
						PendingTextInfo = new TextInfo(txt, selstart + input.Length, text.Length);
					else if( appending )
						PendingTextInfo = new TextInfo(txt, text.Length, 0);
					else
						PendingTextInfo = new TextInfo(txt, selstart + input.Length, 0);

					return true;
				}
			}

			return false;
		}

		#endregion

		#region event handling

		/// <summary>
		/// Called when the linked control is loaded. This registers the event
		/// handlers.
		/// </summary>
		/// 
		/// <param name="sender">The sender of the event.</param>
		/// <param name="e">The event arguments.</param>
		/// 
		private void Control_Loaded( object sender, RoutedEventArgs e )
		{
			if( PickerTextBox != null )
			{
				PickerTextBox.PreviewKeyDown += TextBox_PreviewKeyDown;
				PickerTextBox.PreviewTextInput += TextBox_PreviewTextInput;
				PickerTextBox.TextChanged += TextBox_TextChanged;
			}
		}

		/// <summary>
		/// This is called automatically on a <c>KeyDown</c> event of the control.
		/// This method sets <c>__preventTextChangeValidation</c>.
		/// </summary>
		/// 
		/// <param name="sender">The sender of the event.</param>
		/// <param name="e">The event arguments.</param>
		/// 
		private void TextBox_PreviewKeyDown( object sender, KeyEventArgs e )
		{
			if( Validator == null || PickerTextBox != sender || PickerTextBox.IsReadOnly || e.SystemKey != Key.None )
				return;

			switch( e.Key )
			{
				case Key.Delete:
					if( ControlText.Length > PickerTextBox.SelectionStart )
						__preventTextChangeValidation = true;

					break;

				case Key.Space:
					if( !PickerTextBox.IsReadOnly )
						e.Handled = !HandleInput(" ");

					break;
			}
		}

		/// <summary>
		/// This is called automatically on a <c>KeyPress</c> event of the control.
		/// The text of the control is validated by using <see cref="Validator.IsValidInput(StringBuilder, bool)"/>.
		/// If the input is invalid the key is ignored. <c>e.Handled</c> is set to
		/// <c>true</c> if the pressed key was inserted by this handler.
		/// </summary>
		/// 
		/// <param name="sender">The sender of the event.</param>
		/// <param name="e">The event arguments.</param>
		/// 
		private void TextBox_PreviewTextInput( object sender, TextCompositionEventArgs e )
		{
			if( Validator == null
				|| PickerTextBox != sender
				|| PickerTextBox.IsReadOnly
				|| e.Handled
				|| e.Text.Length == 0
				|| (e.Text == "\t" && !PickerTextBox.AcceptsTab) )
			{
				return;
			}

			e.Handled = !HandleInput(e.Text);
		}

		/// <summary>
		/// This is called if the text of the control is changed. This method validates
		/// the text and formats it if it is valid.
		/// </summary>
		/// 
		/// <param name="sender">The sender of the event.</param>
		/// <param name="e">The event arguments.</param>
		/// 
		private void TextBox_TextChanged( object sender, EventArgs e )
		{
			if( PickerTextBox != sender )
				return;

			if( PendingTextInfo != null )
			{
				TextInfo info = PendingTextInfo;
				PendingTextInfo = null;

				__preventTextChangeValidation = true;

				PickerTextBox.Text = info.Text;

				//
				// Adjusting the selection is necessary because
				// a binding may change the text after is was set:
				//
				int selStart = info.SelectionStart;
				string text = ControlText;

				if( selStart > 0 && text.Length > info.Text.Length )
					selStart += text.Length - info.Text.Length;

				PickerTextBox.Select(selStart, info.SelectionLength);
			}
			else
			{
				try
				{
					if( Validator != null
						&& HasOption(LinkOptions.TextChangeFormats)
						&& FormatTextAllowed
						&& !__preventTextChangeValidation )
					{
						ValidatorException x;

						if( Validator.Validate(ControlText, CultureResolved, out x) )
							FormatText(DatePicker.IsFocused ? TextFormat.Edit : TextFormat.Display);
					}
				}
				finally
				{
					__preventTextChangeValidation = false;
				}
			}
		}

		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
I am a software developer since many years and have worked on several large projects especially in financial sectors and the logistics industry.

My favorite programming languages are C, C++ und newly C#.

I am the architect and chief developer of Tricentis TDM Studio (former Q-up) - a generator that primarily creates template based synthetic data for software testing.

Comments and Discussions