//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 Microsoft.Windows.Controls { 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 } }