Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / WPF

Autocompletion with RichTextBox in WPF as Behavior

Rate me:
Please Sign up or sign in to vote.
4.87/5 (16 votes)
8 Aug 2012CPOL18 min read 39K   2.1K   33   5
This article is describing behaviors ala Intellisense. I'll talk about how to extend the RichTextBox display behavior without interference in the control, without overwriting any of the members, fully configurable in the Xaml code, with the possibility of dynamic control.

Motivation

Often when creating documents you need to insert some of the top established collection items, which after using certain gestures are suggested to him. With regard to the text, Microsoft has implemented a technology known as Intellisense. This is a powerful tool, without which there can imagine themselves so high performance programmers, who are the greatest beneficiaries of this technology unless.

Using WPF (after some corrections works in Silverlight), we can very easily add independent behavior modeled on MS Intellisense. Raises the question of why, when Microsoft delivers with the WPFToolkit and SDK 5 December (for Silverlight) AutoCompleteBox control. I have to say that not to end it meets my expectations. In control is difficult and requires code-behind with advanced cooperation. Rotation control is excellent and works well in most scenarios. The biggest nuisance is that it is an independent rotation control and implementation of its behavior is quite difficult.

In this article I would like to present a slightly different approach to the problem. I'll talk about how to extend the RichTextBox display behavior without interference in the control, without overwriting any of the members, fully configurable in the XAML code, with the possibility of dynamic control. As a canvas uses the implementation of behavior that inserts specific Tags to the edited text, which later another parser converts into concrete data (not implemented).

This article is described only behaviors ala Intellisense. Other behaviors I created here in future articles. The closest will be reflected in styles in cooperating with the RichTextBox display widgets.

How this works

Like the State:

This is the additional window is opened in the cursor position on the host object. This window is to display the specified list from which we can select interesting us the Tag and press the indicated key or clicking the cursor on this item, it will be inserted into the document in the host. After this operation, the window is closed. In addition, has its own element attached to the host context menu, shortcut key, and an incorrect protection mechanism call.

Requirements

For a proper understanding of the principles that I just went here a basic knowledge is required:

  • WPF
  • XAML

The whole is based on the capabilities of WPF objects to the assimilation of different behaviors. -Enhanced cooperation with the elements to create an object based on a BehaviorBase with a strong indication of the class of the object that we will support. In this case, it will be included in the library of the System.Windows.Interactivity class Behavior with a strong typing on a host that is here the RichTextBox display.

using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Controls;
namespace IntellisenseDemo.Behaviorlibrary
{
    public class MyIntellisenseBehavior : Behavior<RichTextBox>
    {
        public MyIntellisenseBehavior()
        {
 
        }
        protected override void OnAttached()
        {
            base.OnAttached();
        }
        protected override void OnDetaching()
        {
            base.OnDetaching();
        }
    }
}

When you add such an object, the object is built and has the capacity to host adoption.

XML
<RichTextBox x:Name="templateRichTextBox">
 <i:Interaction.Behaviors>
   <beh:MyIntellisenseBehavior x:Name="myIntellisense"/>
 </i:Interaction.Behaviors>
</RichTextBox>

In this way, we offer for host new functionality. Of course, at the moment nothing happens. The above in no way intervenes in the host control does not overwrite any of its functionality or its member. It is for the host of completely inert.

To work

Important: the code in this article was completely written by me and the few fragments may overlap with code examples VisualStudioHelp. At the same time, I can inform you that the code in the example is what is and does not take responsibility for the consequences of its misuse. It is a limited part of my larger project and may happen cases of inadvertence functionality or the appropriate reaction. However, were it done analysis of the Contracts and the basic unit tests.

Images sprites thanks: http://www.gentleface.com/free_icon_set.html.

The first step is to create a new project:

  1. Overlooking VisualStudio (I used VS 2010).
  2. If VisualStudio 2010 Express is we call this design WPF and IntellisenseDemo. Editor and solution call IntellisenseDemo. In other cases, we will create a new blank solution first named IntellisenseDemo and later add new project.
  3. We add a new class library project to the solution. Call it MyBehaviorsLibrary. We called it the title of the main bo probably we will attach it solution to other solutions.
  4. We need to add references to the project MyBehaviorsLibrary:
    • WindowsBase
    • PresentationCore
    • PresentationFrame
    • System.Windows.Interactivity (If we don't have it in the system we can acquire from Microsoft website)
    • System.XAML
  5. IntellisenseDemo. Editor in the project to the new library and add a reference to System.Windows.Interactivity.
  6. In a project IntellisenseDemo. Editor opens the file and add a new control in the MainWindow.xaml of the RichTextBox display:
  7. XML
    <Window x:Class="IntellisenseDemo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <RichTextBox>            
            </RichTextBox>
        </Grid>
    </Window>
  8. We add new attribute relationship from the library:
  9. XML
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
  10. Later in the same project, we add the following directories: Model, Extension, Helper and ModelView. This is not necessary, however, in some environments, our light must work.
  11. In the directory Model, we'll add the class model the user (as I mentioned initially will be the behavior of inserting tags):
  12. C#
    public class User
    {
        public User() { }
        public int Id { get; set; }
        public string Name { get; set; }
        public string Surname { get; set; }
        public string PhoneMobil { get; set; }
        public string SecondaryPhone { get; set; }
        public string Job { get; set; }
        public string Sex { get; set; }
        public string DepartmentName { get; set; }
    }
  13. In the directory Extensions will add the Expander class User class
  14. C#
    public static class SuggestionExtension
    {
        public static IEnumerable<string> Suggestions<t>(this T user) where T : class
        {
            var query = from p in user.GetType().GetProperties()
                        select p.Name;
            return query.AsEnumerable();
        }
    }
  15. We will add the class TagsViewModel to the directory of ViewModels:
  16. C#
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Windows.Documents;
    using IntellisenseDemo.Extensions;
    using IntellisenseDemo.Model;
    namespace IntellisenseDemo.ViewModels
    {
        public class TagsViewModel : INotifyPropertyChanged
        {
            #region Private field
            readonly string[] signTemplate = new string[] { "Pozdrawiam\n", 
               "[Sex] [Name] [Surname]\n", "[JobTitle]" };
            readonly ReadOnlyObservableCollection<String> _tagsReadOnly;
            readonly ObservableCollection<string> _tags;
            #endregion
            #region Ctor
            public TagsViewModel()
            {
                Paragraph par = new Paragraph();
                // Insert signTemplate
                par.Inlines.AddRange(new Run[] { new Run(signTemplate[0]), 
                  new Run(signTemplate[1]), new Run(signTemplate[2]) }.AsEnumerable());
                this._signDocument = new FlowDocument(par);
                // ToDo: For Test
                this._tags = new ObservableCollection<string>(new User().Suggestions());
                this._tagsReadOnly = new ReadOnlyObservableCollection<string>(this._tags);
            }
            #endregion
            public ReadOnlyObservableCollection<string> ListTagsToSignature
            {
                get { return _tagsReadOnly; }
            }
            public FlowDocument SignDocument
            {
                get { return _signDocument; }
            }
            private FlowDocument _signDocument;
     
            #region INotifyPropertyChanged Members
            public event PropertyChangedEventHandler PropertyChanged;
            protected void OnPropertyChanged(string propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            #endregion
        }
    }

And if everything you needed on this unless we create the environment. After you build and run (F5) we will start the window with rotation control, which we can write texts but nothing beyond that.

Identify assumptions

We now have evening and we determine what we want our delicacy and contained. And so are the following functionality for the management:

  1. Pane opens with its upper-left corner of the place where the lower-right corner of the rectangle cursor. Mostly, the cursor is presented as a vertical bar, however, our host or the RichTextBox display defines it as a rectangle, therefore such nomenclature.
  2. Pane opens in the following circumstances:
    • When you press certain keys on the keyboard (we will add the ability to define these keys code XAML markup). By the way, all configurations are possible in XAML.
    • The context menu opened by the host window (added to the context menu of the RichTextBox display),
    • By pressing the specified key combination. In this case, Ctrl+J
  3. The pane closes when you press a particular key (also can define in XAML)
  4. List of items in the list is delivered dynamically and is dynamically filtered according to the entered text. IT is not applied to the appropriate culture for adapting functionality here is you have to do the same. The reason is the maximum simplification and highlight the most important tasks. Of course, the culture is one of the most important, however, not in our task of.
  5. It is possible to navigate the keys directly from the host controls pane to the Intellisense and the choice of a specific tag or by mouse click or drag the indicated another device.

To configure specific keys that open window Intellisense, closing and confirming the choice we employ their own XAML markup extension solution. Vision in this later in this article.

The Core Of Intellisense

Expectations are simple so we create the code.

In a project IntellisenseDemo. BehaviorLibrary we MyIntellisenseBehavior class is exactly the same as at the beginning of the article.

IMPORTANT. Not all code is presented in the body of the article, so it should be eating with sources attached to the article.

The main part of each behavior is attached to the behavior of host controls. This is the base class functions in overwritten BehaviorBase. These methods are:

C#
protected override void OnAttached()
{
    base.OnAttached();
}
protected override void OnDetaching()
{
    base.OnDetaching();
}

OnAttached method is invoked at the time of the creation of the host controls. Important: the engine .Net creates first and then tell the children until the main control in the container. This happens with every object IAddChild (and this is the Window or Popup, etc). So remember that not we save on no events of the controls parent host until their creation.

Method of OnDetching and its disintegration or possibly when we disconnect our behavior from host software. In the first we can establish code that waits for the behavior of the host, to attain the specific events, calls, etc. In our case:

C#
protected override void OnAttached()
{
    base.OnAttached();
    // KeyDown is overidde in TextBox and has limited functionality used PreviewKeyDown
    this.AssociatedObject.PreviewKeyDown += new KeyEventHandler(associatedObject_KeyDown);
    this.AssociatedObject.PreviewKeyUp += new KeyEventHandler(associatedObject_PreviewKeyUp);
    this.AssociatedObject.LostFocus += new RoutedEventHandler(associatedObject_LostFocus);
    // Here attaching ContextMenu
    this.AssociatedObject.ContextMenuOpening += new ContextMenuEventHandler(associatedObject_ContextMenuOpening);
    this.AssociatedObject.ContextMenuClosing += new ContextMenuEventHandler(associatedObject_ContextMenuClosing);
    setGesture();
}

In the first line of the time zones to the base object methods. And in turn, we add a reference to the events the keystrokes, the release of keys loss of focus, opening and closing the context menu and finally do features on drawing up gestures to which we will respond. We will respond in a pressing one, move the cursor to the list selector functionality is already an open cursor keys will this pane Intellisense and the down and up. Why only these and why press and why PreviewKeyDown and KeyDown does not. First, we have to stay one step ahead of the host and take control of the event and move the focus to the opened pane, so you can safely navigate in it. Secondly, the host or the RichTextBox display itself changes the functionality of the keys and invalidate the possibility of using the cursor keys and several other leaving alone PreviewKeyDown Tunnel that uses the strategy and is fully available to our raw (the same applies to the release keys). Release the keys to use to open, close and check list (panes).Why the exemption is we want to take the text entered by the user to control the host so we have to wait until the host organizes related tasks and we take checks over these events. Opening and closing the context menu event is obvious, and will remain, to discuss the establishment of gestures the reaction of our behavior. It will do on the occasion to discuss the DelegateCommand.

To accede to explain further action we should use internal resources devoted to a behavior object. Most of the variables is private for the entire class, and are made available to all methods-members in class (with the exception of the static members for obvious reasons). The main philosophy is to work on only one set of variables that are in turn processed by individual functionality. Uses the fact that everything is closed in a single structure and does not require the evaluation of data transferred for easy work and tests (this could hamper the unit tests are important because fragmentation is without a host object, and so it would not be possible on one set, we can test all methods, unfortunately not taking it with the project any tests.

Here are the main variables of the class resources:

C#
// main items
Popup _container;
protected Selector _Child;
protected Panel _Panel;
//intellisense core
int _filterIndex = 0;
object _selectedValue;
List<string> _filterList;
//intellisense core for insert
TextPointer _insertStart, _insertEnd;
// Context menu
Control _intellisenseContextMenu;
Separator _sep;
BehaviorDelegateCommand _open;
InputBinding _ib_open;
Image _icon;

Let: Popup _container; is the main container of the pane. We do not use this variable in classes that inherit from this behavior. Variables protected Selector _Child;, protected Panel _Panel; are shared objects that inherit from a class and represent the selector panel in which the selector, and that seemed like the popup pane. Popup is a CONTENT control (that is, content) enables us to add only one child. In our example we use not more than one child, however, I'll leave this Panel can add further items to the Panel, for example, the rectangle to move the fields to the presentation explanation (of course we can implement additional TipTool), etc. Then we have the index of the selected item from the filtered list and the selected object. List of filterList represents a list that is currently in our data source pane. TextPointer _insertStart, _insertEnd; TextPointer class are instances that define the location of the beginning and end of the selected text or object in the host control. Also means a place in which the including our tag. The variable intellisenseContextMenu of type Control is the place where your our menu object that has been added to the context menu of the host and allows us to find it in the list of objects in this menu because it probably won't be the only customers host context menu. The variable _ib_open of type InputBinding is a representation of the shortcut key combination called the gesture, which will enable us to open our popup using Crtl+J shortcut because so is giving. _icon stores the image, which is placed on the context menu. At the end of the remaining US instance of class BehaviorDelegateCommand, which represents the internally used command to support gestures shortcuts and click the trackball.

C#
using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Input;
namespace Behaviorlibrary
{
    internal class BehaviorDelegateCommand : ICommand
    {
        // Specify the keys and mouse actions that invoke the command. 
        public Key GestureKey { get; set; }
        public ModifierKeys GestureModifier { get; set; }
        public MouseAction MouseGesture { get; set; }
        public string InputGestureText { get; set; }
        Action<object> _executeDelegate;
        Func<object, bool> _canExecuteDelegate;
        public BehaviorDelegateCommand(Action<object> executeDelegate)
            : this(executeDelegate, null){}
        public BehaviorDelegateCommand(Action<object> executeDelegate, 
                   Func<object, bool> canExecuteDelegate)
        {
            //Contract.Requires<ArgumentNullException>(executeDelegate == null);
            _executeDelegate = executeDelegate;
            _canExecuteDelegate = canExecuteDelegate;
        }
        public void Execute(object parameter)
        {
            _executeDelegate(parameter);
        }
        public bool CanExecute(object parameter)
        {
            return _canExecuteDelegate(parameter);
        }
        [SuppressMessage("Microsoft.Contracts", "CS0067", 
          Justification = "The event 'BehaviorDelegateCommand.CanExecuteChanged' is never used.")]
        public event EventHandler CanExecuteChanged;
    }
}

You now look at the constructor, which creates most of the instance variables with specific values:

C#
public MyIntellisenseBehavior()
{
     _container = null;
     _Child = null;
     _selectedValue = null;
     _insertStart = null;
     _insertEnd = null;
     _filterList = new List<string>();
     _icon = new Image();
     _icon.BeginInit();
     _icon.Source = new BitmapImage(new Uri(
       @"/Behaviorlibrary;component/brackets_icon16.png", 
       UriKind.RelativeOrAbsolute));
     _icon.EndInit();
     _open = new BehaviorDelegateCommand(OnOpen, CanOnOpen)
     {
       // internal using
       GestureKey = Key.J,
       GestureModifier = ModifierKeys.Control,
       MouseGesture = MouseAction.LeftClick,
       InputGestureText = "Ctrl+J"
     };
}

Actually, everything here is clear except for creating an object variable as the DelegateCommand _open. We create instances by passing to the constructor of the two delegaty methods; the first performing job responses to it, and with the help of _ib_open gestures in the OnAttached method we added these gestures to the System Manager; the second delegate to a method that checks whether it is possible to respond to this gesture. InputGestureText = "Ctrl + J" represents a Visual item in the context menu. In other words, when we are pushing Ctrl + J opens us to the pane with our list of ala Intellisense or when clicked context menu added our heading. When the pane is open, it is not possible to respond to shortcut keys, the system simply does not transmit to us, and the position in the menu is locked (muted but there).

A further key filters:

C#
// handle key
void associatedObject_PreviewKeyDown(object sender, KeyEventArgs e)
{
    // The Focus for the ListBox in popup
    if ((e.Key == Key.Down || e.Key == Key.Up) && _container != null) this._Child.Focus();
}
// handle key
[SuppressMessage("Microsoft.Contracts", "CC1015", 
  MessageId = "if (_container == null) return", 
  Justification = "Return statement found in contract section.")]
[SuppressMessage("Microsoft.Contracts", "CC1057", 
  MessageId = "KeyEventArgs e", 
  Justification = "Has custom parameter validation but assembly mode 
    is not set to support this. It will be treated as Requires<e>.")]
void associatedObject_PreviewKeyUp(object sender, KeyEventArgs e)
{
Key m_key = e.Key;
// Initialization popup
if (KeysOpened == null) throw new ArgumentNullException("KeysOpened");
if (_container == null && (this.KeysOpened.Count() >= 0 && 
   this.KeysOpened.Any(k => k == m_key))) startIntellisense();// Generate intellisense
// CC1015 = "Return statement found in contract section."
if (_container == null) { this.endIntellisense(); return; }
// is wanting the closing
if (KeysClosed == null) throw new ArgumentNullException("KeysClosed");
if (this.KeysClosed.Count() >= 1 && this.KeysClosed.Any(k => k == m_key)) this.endIntellisense();// Closed intellisense
 // core the work
 if (KeysReturned == null) throw new ArgumentNullException("KeysReturned");
 if (_container != null && !this.KeysReturned.Any(k => k == m_key)) coreIntellisense();// logic intellisense
 // to selecting
if (this.KeysReturned.Count() >= 1 && this.KeysReturned.Any(k => k == m_key)) insertValue();  // Key to selecting
}

Subsequent tests to control the position of the focus. I need to admit that there are some "lost" but they are due to the State of the system and other applications and the way in which we are in the application. For simple applications, everything is in order, however, very complex when we have several behaviors and subsystems controlling the position of the focus there are "divine preservation – only God knows where is the focus".

C#
void associatedObject_LostFocus(object sender, RoutedEventArgs e)
{
    // _no exist
    if (_Child == null)
    {
        this.IsOpen = false;
        return;
    }
    // Where is focus
    var m_wind = GetAncestorTop<Window>(this.AssociatedObject);
    IInputElement m_inputElem = FocusManager.GetFocusedElement(m_wind);

    // If selected mouse or other touch device
    if (typeof(ListBoxItem) == m_inputElem.GetType())
    {
        var m_testAnces = GetAncestorTop<ListBox>(m_inputElem as ListBoxItem);
        if (_Child.Equals(m_testAnces)) return;
    };
    // is exist
    if (!_Child.Equals(m_inputElem))
        this.IsOpen = false;
}

Remember that the user can close the application in different ways, so not to be with references and resources we need to handle not closed:

C#
private void closeAncestorTopHandler()
{
  GetAncestorTop<window>(this.AssociatedObject).Closing += 
        new System.ComponentModel.CancelEventHandler(topWindow_Closing);
}
void topWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
   endIntellisense();
}

We drive in the function startIntellisense() and every time you close-released resources. We do this in the method endIntellisense().

Yet we're only adding to and subtracting the capacity from the context menu and attach themselves to the host on the gestures.

C#
private void setGesture()
{
    _ib_open = new InputBinding(
        _open,
        new KeyGesture(
            _open.GestureKey,
            _open.GestureModifier));
    this.AssociatedObject.InputBindings.Add(_ib_open);
}

Here I include the collection of bindings providing text input device event Manager. Keyboard. When the manager finds that has interesting events on the keyboard will perform the method from a delegate _open i.e. delegate BehaviorDelegateCommand.

C#
void associatedObject_ContextMenuClosing(object sender, ContextMenuEventArgs e)
{ 
    	// removing all intellisense items
    	this.AssociatedObject.ContextMenu.Items.Remove(_intellisenseContextMenu);
    	this.AssociatedObject.ContextMenu.Items.Remove(_sep);
}
void associatedObject_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
  if (_intellisenseContextMenu == null)
  {
     MenuItem menuItem = new MenuItem()
      {
        Header = "Intellisense",
        InputGestureText = this._open.InputGestureText,
        Command = this._open,
        Icon = _icon
      };
 
  // first separator
  this.AssociatedObject.ContextMenu.Items.Add(_sep = new Separator());
  // temp pointer for removing
  _intellisenseContextMenu = menuItem;
  // now item
  this.AssociatedObject.ContextMenu.Items.Add(_intellisenseContextMenu);
 }
}

These two methods are responsible for working with the host context menu. The method listed in the response to the opening creates a menu item, gave it the title of Intellisense, provides a testing match the gesture or the "Ctrl + J", assigns the command to execute at the time of the gesture and image attached to an item. We add the separator and we retain a reference to the control named intellisenseContextMenu which will serve a method which subtracts the menu item to identify our item in a context menu.

Now you prepare our Popup or a container which will show you the list of tags you will be able to select the correct.

C#
void startIntellisense()
{
     closeAncestorTopHandler();
     // creating instantion visual
     InitUIElements();
     _container = new Popup();
     ((IAddChild)_container).AddChild(_Panel);
     // set functionality child - attach events
     functionalityDefaultChild();
     // layout popup
     _container.Placement = PlacementMode.Custom;
     _container.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(placePopup);
     _container.PlacementTarget = this.AssociatedObject;
     // default setting data source for list to display
     if (this.ItemsSource != null)
     {
         this._filterList = this.ItemsSource.Cast<string>().OrderBy(s => s).ToList();
     }
     if (!this.IsOpen) this.IsOpen = true;
     // appears popup
     _container.IsOpen = true;
}

As I mentioned earlier, we have to ensure that at the time of closing the host not be with references an open window. Just how well the GC will check for references and follow our translated into other objects and finally removes them and how well it goes. So I will add your subscription on closing the main event and I resources in method endIntellisense().

Next we initialize the values our visual elements. And we add them to the container.

C#
/// <summary>
/// In a class that inherits from this class overriding this method, you can change some graphics. 
/// The panel inside the popup, and selector.
/// </summary>
protected virtual void InitUIElements()
{
     _Panel = new StackPanel();
     _Child = new ListBox();
     _Panel.Children.Add(_Child);
}

Then we need to add some functionality to our UI. We'll add your subscription several events our selector (ListBox). If the user clicks the mouse cursor on the element, we choose the location in space of the host and we pass to the method that inserts the specified selection to the text of the host.

C#
// Add handle to list selector
private void functionalityDefaultChild()
{
 // PreviewMouseDoubleClick because MouseDoubleClick not is for me using
 _Child.PreviewMouseDoubleClick += (s_child, e_child) =>
 {
     _insertStart = this.AssociatedObject.CaretPosition;
     insertValue();
 };
 // no coments
 _Child.SelectionChanged += (s_child, e_child) => setSelectedValue();
 // as above MouseDoubleClick
 _Child.PreviewKeyDown += (s_child, e_child) =>
 {
     switch (e_child.Key)
     {
         default:
             this.AssociatedObject.Focus();
             this.IsOpen = false;
             break;
         case Key.Down:
             break;
         case Key.Up:
             break;
         case Key.Tab:
             insertValue();
             e_child.Handled = true;
             this.AssociatedObject.Focus();
             break;
     }
 };
 // correct size
 if (_width >= 0 || double.IsNaN(_width))
     _Child.Width = _width;
 if (_height >= 0 || double.IsNaN(_height))
     _Child.Height = _height;
}

The event keys control provides a list selector. Here the Tab key has been boiled encoded as selection key. I've done this more simply because the idea is simple to change the dynamic, but it is very difficult for us to read the code.

Heart preservation

At the heart of our Intellisence is a method coreIntellisense (), which determine the place at which to insert the specified tag. With the help of the host position indicator derived from the position in the text, we need to adjust the position of the original. As we know, our behavior is a reaction post facto on the keys, and the host of each introduction of a new character changes position indicator which forces us to reducing its index of one character. We only one-off set starting position indicator and this irrespective of the type of the initialization behavior. If we will do this in the context menu is the position is fixed as is and we will not adjust it or keyboard, and this requires correction.

The end of the pointer is adjusted dynamically. In the next step of our indicators are indicated by the text as a basis for the selection of the filter. Here also will change which characters should be cleaned, boiled with the text filter. Next we have the asynchronous filtering list because it may happen that we want to use fairly complex filtering, so the possibility of further decisions of the notice to the user.

C#
void coreIntellisense()
{
    // start position to inserting tag
    if (_insertStart == null) _insertStart = 
      this.AssociatedObject.CaretPosition.GetPositionAtOffset(-1, LogicalDirection.Backward);
    // ending selection to reverse at new run
    _insertEnd = this.AssociatedObject.CaretPosition;
    string m_textFilter = new TextRange(_insertStart, _insertEnd).Text;
    // this is Hard Code
    // TODO: trim from list KeysOpened and KeysClosed as will request
    string m_newFlter = m_textFilter.Trim(new char[] { '[', '{', ' ', ']', '}' });
    IEnumerable<object> m_newFilter = null;
    // set filterList to visable
    _Child.ItemsSource = this._filterList;
    // asynch
    this.Dispatcher.BeginInvoke(
        new Action(() =>
        {
            if (!String.IsNullOrWhiteSpace(m_newFlter))
            {
                //update filterList
                m_newFilter = this._filterList.Where(s => s.ToLower().StartsWith(m_newFlter.ToLower()));
                // set filterList to visable
                if (m_newFilter != null && m_newFilter.Count() > 0) _Child.ItemsSource = m_newFilter;
                    // actual index in list
                      _Child.SelectedIndex = this._filterIndex;
                 }
                }));
            // actual index in list
       _Child.SelectedIndex = this._filterIndex;
}

In the method of auxiliary set value to members, which may show the current status of our behavior to other participants of the subsystem.

C#
// Actualize values selecting 
void setSelectedValue()
{
    // selected index
    _filterIndex = _Child.SelectedIndex;
    this.SelectedIndex = _filterIndex;
    _selectedValue = _Child.SelectedItem;
    this.SelectedItem = _selectedValue;
    // this system ignore args
    this.OnSelectedItemChanged(null);
}

The method insertValue() were consumed everything whatever we insert the specified text to the tag and ends with the host.

C#
// Inserting selected tip
void insertValue()
{
     this.AssociatedObject.CaretPosition = _insertStart;
     this.AssociatedObject.Selection.Select(_insertStart, _insertEnd);
     this.AssociatedObject.Selection.Text = String.Empty;
     setSelectedValue();
     this.AssociatedObject.CaretPosition.InsertTextInRun(String.Format("[{0}] ", _selectedValue));
            this.AssociatedObject.Focus();
            endIntellisense();
}

Feet fun

Nevertheless, important, and maybe more important, is to leave after each order. The exemption of a maximum quantity of resources with most delete useless references. Remember that our garbage truck as the main criterion of suitability of object has just links. It may take a little bit of work the GC it finds there our junk. So as the environmentalists themselves we collect junk. Application of the pattern is not necessary here. IDispose Just plain "nothing"-"null" and disconnect subscription.

C#
void endIntellisense()
{
    this._filterIndex = 0;
    this._filterList = null;
    this._insertEnd = null;
    this._insertStart = null;
    this.IsOpen = false;
    _Child = null;
    _Panel = null;
    _container = null;
    GetAncestorTop<Window>(this.AssociatedObject).Closing -= 
       new System.ComponentModel.CancelEventHandler(topWindow_Closing);
}

And probably everything from these main goals. A little work remains to us, about the task force.

IsOpen

If you need to attach to the main Menu of the application or another trigger tasks (including testing UIAutomation) our behaviors are developed has the functionality named IsOpen. IsOpen accepts two values, true or false. When IsOpen is true, map to a member, it will be initialized system Intellisense, false finishes its work. At the same time notify subscriptions to its participants about the current events in their State. The functionality is based on the DependencyProperty could not be registered and you can integrate it in XAML markup environment complete with all its benefits and consequences.

C#
#region DependencyProperty IsOpen
[CustomPropertyValueEditorAttribute(System.Windows.Interactivity.CustomPropertyValueEditor.PropertyBinding)]
public bool IsOpen
{
    get { return (bool)GetValue(IsOpenProperty); }
    set { SetValue(IsOpenProperty, value); }
}
// Using a DependencyProperty as the backing store for IsOpen. This enables binding.
public static readonly DependencyProperty IsOpenProperty =
    DependencyProperty.Register("IsOpen", typeof(bool), typeof(MyIntellisenseBehavior),
    new UIPropertyMetadata(false,
        new PropertyChangedCallback(onIsOpenChanged),
        new CoerceValueCallback(coerceValue)));
private static object coerceValue(DependencyObject element, object value)
{
    bool newValue = (bool)value;
    return newValue;
}
private static void onIsOpenChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    MyIntellisenseBehavior control = (MyIntellisenseBehavior)obj;
    RoutedPropertyChangedEventArgs<bool> e = new RoutedPropertyChangedEventArgs<bool>(
        (bool)args.OldValue, (bool)args.NewValue, IsOpenChangedEvent);
    control.OnIsOpenChanged(e);
}
#endregion

ItemsSource

This is the DependencyProperty could not be registered to provide the list of objects that will be displayed on the list, Intellisense.

C#
#region DependencyProperty ItemsSource
// Attribute for Blend Expression        
[CustomPropertyValueEditorAttribute(System.Windows.Interactivity.CustomPropertyValueEditor.PropertyBinding)]
  public IEnumerable<object> ItemsSource
  {
      get { return (IEnumerable<object>)GetValue(ItemsSourceProperty); }
      set { SetValue(ItemsSourceProperty, value); }
  }
  // Using a DependencyProperty as the backing store for ItemsSource.  This enables binding.
  public static readonly DependencyProperty ItemsSourceProperty =
      DependencyProperty.Register("ItemsSource", typeof(IEnumerable<object>), 
        typeof(MyIntellisenseBehavior), new UIPropertyMetadata(new List<object>()));
#endregion

There are also other functionality not described in the article:

  • SelectedIndex
  • SelectedItem
  • region Property Size

In the client code

To all this work we have yet to assimilate our behavior to the host. A good way is to use XAML. Using the method of Interaction, we add to the library of other behaviors. First we need to inform the client about our resources by adding them to the file header.

XML
xmlns:beh="clr-namespace:BehaviorLibrary;assembly=BehaviorLibrary"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Refactoring of previously written code for the following:

XML
<RichTextBox x:Name="templateRichTextBox"
             AcceptsTab="True"
             VerticalScrollBarVisibility="Auto">
    <RichTextBox.ContextMenu>
        <ContextMenu x:Name="contextMenu">
            <MenuItem Command="ApplicationCommands.Undo"
                      Header="Undo"
                      Icon="{StaticResource undoImage}"
                      Style="{StaticResource cMenuButton}" />
            <MenuItem Command="ApplicationCommands.Redo"
                      Header="Redo"
                      Icon="{StaticResource redoImage}"
                      Style="{StaticResource cMenuButton}" />
            <Separator />
            <MenuItem Command="ApplicationCommands.Cut"
                      Header="Cut"
                      Style="{StaticResource cMenuButton}"
                      Icon="{StaticResource cutImage}" />
            <MenuItem Command="ApplicationCommands.Copy"
                      Header="Copy"
                      Style="{StaticResource cMenuButton}"
                      Icon="{StaticResource copyImage}" />
            <MenuItem Command="ApplicationCommands.Paste"
                      Header="Paste"
                      Style="{StaticResource cMenuButton}"
                      Icon="{StaticResource pasteImage}" />
        </ContextMenu>
    </RichTextBox.ContextMenu>
    <i:Interaction.Behaviors>
        <beh:RichTextBoxIntellisense x:Name="rtbIntelliseanse"
                                     KeysOpened="{beh:KeysFromChar ‘[,OemPipe}’"
                                     KeysClosed="{beh:KeysFromChar ]\,OemBackslash\,Escape}"
                                     KeysReturned="{beh:KeysFromChar Tab Return}"
                                     ItemsSource="{Binding ListTagsToSignature}" />
    </i:Interaction.Behaviors>
    <FlowDocument>
        <Paragraph />
    </FlowDocument>
</RichTextBox>

Light is equipped with a host of in context menu to which we dynamically add and subtract just as dynamically. Nothing in this matter already do not need. The whole will address our instance with new behaviors. Attention in the same declaration of new behavior strange syntax.

C#
KeysOpened="{beh:KeysFromChar ‘[,OemPipe}’"
KeysClosed="{beh:KeysFromChar ]\,OemBackslash\,Escape}"
KeysReturned="{beh:KeysFromChar Tab Return}"

These are the declarations of the letter corresponding to the set of keys for opening, closing and approval of choice. Braces using written specially for this behavior to represent MarkupExtension values, which is responsible for reading, conversion and transfer to the appropriate variables indicated values. We see three ways to declare the values passed to the constructor to represent MarkupExtension values. The first way of transmission is treated as string ' [, OemPipe ', encoding in the body of the constructor, the second is also a string of type string, as this requires a constructor and is equivalent to '], OemBackslash, Escape ' backslash characters are required by Expression Blend, which is the same as when it converts us into this we will edit the client in this program. About behaviors and Expression Blend follows next. The third and final variant is seen by represent MarkupExtension values as "Tab Return" or a string of words separated by a space. Settings keys, downloading them from XAML markup.

C#
/// <summary>
/// Extension for xaml. Specially written for RichTextBoxIntellisense.
/// </summary>
public sealed class KeysFromCharExtension : MarkupExtension
{
    //IDictionary<string, Key> dictionaryKey = KeysFromCharExtension.GetDictionaryKeys();
    Func<string, Key> _comparer;
    IEnumerable<Key> _keys;
    private string[] _keysChar;
    public KeysFromCharExtension(String keysChar)
    {
        if (String.IsNullOrEmpty(keysChar)) throw new ArgumentNullException("keysChar");
       // hard code char ' ' and ',' only
       if (keysChar.Any(c => c == ',')) _keysChar = keysChar.Trim(' ').Split(',');
       else _keysChar = keysChar.Trim(' ').Split();
       _keys = new List<Key>();
       _comparer = ConvertKeyFromString;
       _keys = _keysChar.Select(k => ConvertKeyFromString(k.Trim(' '))).ToList();
   }
   // Important
   //Key ConvertKeyFromString(string stringKey, CultureInfo culture)
   Key ConvertKeyFromString(string stringKey)
   {
       Key m_key;
       KeyConverter cov = new KeyConverter();
       m_key = KeysFromCharExtension.GetDictionaryCodeKeys().FirstOrDefault(k => k.Key == stringKey).Value;
       if (m_key == Key.None)
           try
           {
               m_key = (Key)cov.ConvertFromInvariantString(stringKey.ToUpper());
           }
           catch
           {
               // if not know set to Key.None
               m_key = Key.None;
           }
       return m_key;
   }
   public override object ProvideValue(IServiceProvider serviceProvider)
   {
       return _keys;
   }
}

Behaviors in Expression Blend

When editing our solution in Expression Blend 4, we can configure our behavior intuitively using the Inspector.

Object picker and the Inspector of the selected object.

Our Intellisense is fully fledged component of our applications with the ability to use other technologies without additional conversion.

Adding to our library of classes to the behavior as an open project in the solution, or as a reference to Assembly (.dll) with access to the preserve in the list of Assets in Expression Blend, and after adding it to the host, we have full access to its properties and working through the configuration object Inspector.

Feel free to read the next episode in which the identified behavior designed to reflect the style that is assigned to the text that is currently in the possession of the indicator and show this in the cooperating widgets. This type of font, size, thickness, effects, color letters and background etc.

Best regards, Andrzej Skutnik

License

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


Written By
freelancer
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionSilverlight Pin
juan-ito21-Jan-13 8:58
juan-ito21-Jan-13 8:58 
AnswerRe: Silverlight Pin
Andrzej Skutnik13-Feb-13 5:28
Andrzej Skutnik13-Feb-13 5:28 
GeneralMy vote of 3 Pin
sam.hill8-Aug-12 17:31
sam.hill8-Aug-12 17:31 
GeneralRe: My vote of 3 Pin
Andrzej Skutnik8-Aug-12 20:43
Andrzej Skutnik8-Aug-12 20:43 
GeneralRe: My vote of (4) Pin
sam.hill9-Aug-12 5:00
sam.hill9-Aug-12 5:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.