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.
394 lines
12 KiB
394 lines
12 KiB
/************************************************************************
|
|
|
|
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
|
|
}
|
|
}
|