/************************************************************************ Extended WPF Toolkit Copyright (C) 2010-2012 Xceed Software Inc. This program is provided to you under the terms of the Microsoft Reciprocal License (Ms-RL) as published at http://wpftoolkit.codeplex.com/license This program can be provided to you by Xceed Software Inc. under a proprietary commercial license agreement for use in non-Open Source projects. The commercial version of Extended WPF Toolkit also includes priority technical support, commercial updates, and many additional useful WPF controls if you license Xceed Business Suite for WPF. Visit http://xceed.com and follow @datagrid on Twitter. **********************************************************************/ //Based of the code written by Pavan Podila //http://blog.pixelingene.com/2010/10/tokenizing-control-convert-text-to-tokens/ using System; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; namespace Xceed.Wpf.Toolkit { public class TokenizedTextBox : ItemsControl { #region Members private System.Windows.Controls.RichTextBox _rtb = null; private bool _surpressTextChanged = false; private bool _surpressTextChangedEvent = false; #endregion //Members #region Properties public static readonly DependencyProperty SearchMemberPathProperty = DependencyProperty.Register( "SearchMemberPath", typeof( string ), typeof( TokenizedTextBox ), new UIPropertyMetadata( String.Empty ) ); public string SearchMemberPath { get { return ( string )GetValue( SearchMemberPathProperty ); } set { SetValue( SearchMemberPathProperty, value ); } } public static readonly DependencyProperty TokenDelimiterProperty = DependencyProperty.Register( "TokenDelimiter", typeof( string ), typeof( TokenizedTextBox ), new UIPropertyMetadata( ";" ) ); public string TokenDelimiter { get { return ( string )GetValue( TokenDelimiterProperty ); } set { SetValue( TokenDelimiterProperty, value ); } } public static readonly DependencyProperty TokenTemplateProperty = DependencyProperty.Register( "TokenTemplate", typeof( DataTemplate ), typeof( TokenizedTextBox ), new UIPropertyMetadata( null ) ); public DataTemplate TokenTemplate { get { return ( DataTemplate )GetValue( TokenTemplateProperty ); } set { SetValue( TokenTemplateProperty, value ); } } #region Text public static readonly DependencyProperty TextProperty = DependencyProperty.Register( "Text", typeof( string ), typeof( TokenizedTextBox ), new UIPropertyMetadata( null, OnTextChanged ) ); public string Text { get { return ( string )GetValue( TextProperty ); } set { SetValue( TextProperty, value ); } } private static void OnTextChanged( DependencyObject o, DependencyPropertyChangedEventArgs e ) { TokenizedTextBox tokenizedTextBox = o as TokenizedTextBox; if( tokenizedTextBox != null ) tokenizedTextBox.OnTextChanged( ( string )e.OldValue, ( string )e.NewValue ); } protected virtual void OnTextChanged( string oldValue, string newValue ) { if( _rtb == null || _surpressTextChanged ) return; //TODO: when text changes update tokens } #endregion //Text public static readonly DependencyProperty ValueMemberPathProperty = DependencyProperty.Register( "ValueMemberPath", typeof( string ), typeof( TokenizedTextBox ), new UIPropertyMetadata( String.Empty ) ); public string ValueMemberPath { get { return ( string )GetValue( ValueMemberPathProperty ); } set { SetValue( ValueMemberPathProperty, value ); } } #endregion //Properties #region Constructors static TokenizedTextBox() { DefaultStyleKeyProperty.OverrideMetadata( typeof( TokenizedTextBox ), new FrameworkPropertyMetadata( typeof( TokenizedTextBox ) ) ); } public TokenizedTextBox() { CommandBindings.Add( new CommandBinding( TokenizedTextBoxCommands.Delete, DeleteToken ) ); } #endregion //Constructors #region Base Class Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); if( _rtb != null ) { _rtb.TextChanged -= RichTextBox_TextChanged; _rtb.PreviewKeyDown -= RichTextBox_PreviewKeyDown; } _rtb = GetTemplateChild( "PART_ContentHost" ) as System.Windows.Controls.RichTextBox; if( _rtb != null ) { _rtb.TextChanged += RichTextBox_TextChanged; _rtb.PreviewKeyDown += RichTextBox_PreviewKeyDown; } InitializeTokensFromText(); } #endregion //Base Class Overrides #region Event Handlers private void RichTextBox_TextChanged( object sender, TextChangedEventArgs e ) { if( _surpressTextChangedEvent ) return; var text = _rtb.CaretPosition.GetTextInRun( LogicalDirection.Backward ); var token = ResolveToken( text ); if( token != null ) { ReplaceTextWithToken( text.Trim(), token ); } } void RichTextBox_PreviewKeyDown( object sender, KeyEventArgs e ) { InlineUIContainer container = null; if( e.Key == Key.Back ) { container = _rtb.CaretPosition.GetAdjacentElement( LogicalDirection.Backward ) as InlineUIContainer; } else if( e.Key == Key.Delete ) { //if the selected text is a blank space, I will assume that a token item is selected. //if a token item is selected, we need to move the caret position to the left of the element so we can grab the InlineUIContainer if( _rtb.Selection.Text == " " ) { TextPointer moveTo = _rtb.CaretPosition.GetNextInsertionPosition( LogicalDirection.Backward ); _rtb.CaretPosition = moveTo; } //the cursor is to the left of a token item container = _rtb.CaretPosition.GetAdjacentElement( LogicalDirection.Forward ) as InlineUIContainer; } //if the container is not null that means we have something to delete if( container != null ) { var token = ( container as InlineUIContainer ).Child as TokenItem; if( token != null ) { SetTextInternal( Text.Replace( token.TokenKey, "" ) ); } } } #endregion //Event Handlers #region Methods private void InitializeTokensFromText() { if( !String.IsNullOrEmpty( Text ) ) { string[] tokenKeys = Text.Split( new string[] { TokenDelimiter }, StringSplitOptions.RemoveEmptyEntries ); foreach( string tokenKey in tokenKeys ) { var para = _rtb.CaretPosition.Paragraph; var token = new Token( TokenDelimiter ) { TokenKey = tokenKey, Item = ResolveItemByTokenKey( tokenKey ) }; para.Inlines.Add( CreateTokenContainer( token ) ); } } } private Token ResolveToken( string text ) { if( text.EndsWith( TokenDelimiter ) ) return ResolveTokenBySearchMemberPath( text.Substring( 0, text.Length - 1 ).Trim() ); return null; } private Token ResolveTokenBySearchMemberPath( string searchText ) { //create a new token and default the settings to the search text var token = new Token( TokenDelimiter ) { TokenKey = searchText, Item = searchText }; if( ItemsSource != null ) { foreach( object item in ItemsSource ) { var searchProperty = item.GetType().GetProperty( SearchMemberPath ); if( searchProperty != null ) { var searchValue = searchProperty.GetValue( item, null ); if( searchText.Equals( searchValue.ToString(), StringComparison.InvariantCultureIgnoreCase ) ) { var valueProperty = item.GetType().GetProperty( ValueMemberPath ); if( valueProperty != null ) token.TokenKey = valueProperty.GetValue( item, null ).ToString(); token.Item = item; break; } } } } return token; } private object ResolveItemByTokenKey( string tokenKey ) { if( ItemsSource != null ) { foreach( object item in ItemsSource ) { var property = item.GetType().GetProperty( ValueMemberPath ); if( property != null ) { var value = property.GetValue( item, null ); if( tokenKey.Equals( value.ToString(), StringComparison.InvariantCultureIgnoreCase ) ) return item; } } } return tokenKey; } private void ReplaceTextWithToken( string inputText, Token token ) { _surpressTextChangedEvent = true; var para = _rtb.CaretPosition.Paragraph; var matchedRun = para.Inlines.FirstOrDefault( inline => { var run = inline as Run; return ( run != null && run.Text.EndsWith( inputText ) ); } ) as Run; if( matchedRun != null ) // Found a Run that matched the inputText { var tokenContainer = CreateTokenContainer( token ); para.Inlines.InsertBefore( matchedRun, tokenContainer ); // Remove only if the Text in the Run is the same as inputText, else split up if( matchedRun.Text == inputText ) { para.Inlines.Remove( matchedRun ); } else // Split up { var index = matchedRun.Text.IndexOf( inputText ) + inputText.Length; var tailEnd = new Run( matchedRun.Text.Substring( index ) ); para.Inlines.InsertAfter( matchedRun, tailEnd ); para.Inlines.Remove( matchedRun ); } //now append the Text with the token key SetTextInternal( Text + token.TokenKey ); } _surpressTextChangedEvent = false; } private InlineUIContainer CreateTokenContainer( Token token ) { return new InlineUIContainer( CreateTokenItem( token ) ) { BaselineAlignment = BaselineAlignment.Center }; } private TokenItem CreateTokenItem( Token token ) { object item = token.Item; var tokenItem = new TokenItem() { TokenKey = token.TokenKey, Content = item, ContentTemplate = TokenTemplate }; if( TokenTemplate == null ) { //if no template was supplied let's try to get a value from the object using the DisplayMemberPath if( !String.IsNullOrEmpty( DisplayMemberPath ) ) { var property = item.GetType().GetProperty( DisplayMemberPath ); if( property != null ) { var value = property.GetValue( item, null ); if( value != null ) tokenItem.Content = value; } } } return tokenItem; } private void DeleteToken( object sender, ExecutedRoutedEventArgs e ) { var para = _rtb.CaretPosition.Paragraph; Inline inlineToRemove = para.Inlines.Where( inline => inline is InlineUIContainer && ( ( inline as InlineUIContainer ).Child as TokenItem ).TokenKey.Equals( e.Parameter ) ).FirstOrDefault(); if( inlineToRemove != null ) para.Inlines.Remove( inlineToRemove ); //update Text to remove delimited value SetTextInternal( Text.Replace( e.Parameter.ToString(), "" ) ); } private void SetTextInternal( string text ) { _surpressTextChanged = true; Text = text; _surpressTextChanged = false; } #endregion //Methods } }