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.

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
}
}