Introduction
Today, intellisense becomes a must in a whole range of different applications. Applications become smarter due to this enormous unharvested CPU power that is idling on your desktop and the applications find a way to use it.
Using the Code
The technique that I'm going to explain here is a user-friendly way to hint or help the user with her or his next actions while she or he works on the primary task. For example:
- The user types a text, a spell checker (which is at your fingertips in WPF) finds the misspelled words and the application can suggest a choice to the user.
- The user types a script and the application lets the user choose a next command from a list of possibilities.
- Etc.
We can go on with the list, but, I hope, you got the idea. In my case, I'll show you how to help the user construct a LINQ query.
To keep the explanation short and to the point, let's scope the problem: the application will show a popup only when the user types a predefined keyword and presses a ".
" (dot). The popup will contain a list of possible methods that can be applied to the object.
Let's create a class that will provide a list of the item's methods. Let's say that the item is of type SyndicationItem
. So we'll use reflection methods (if you don't know what reflection is, you should look into it, it's very useful in a whole range of applications) to find all the methods of the SyndicationItem
type. You can use a .GetType()
of an object or you can use a simple typeof(...)
construct. The following code snippet does just this:
1: public class ItemMethods
2: {
3: public IEnumerable<string> Methods
4: {
5: get
6: {
7: Type type = typeof(SyndicationItem);
8:
9: var methods = (from method in
10: type.GetMethods(System.Reflection.BindingFlags.Public |
11: System.Reflection.BindingFlags.Instance)
12: select method.Name).ToList<string>();
13:
14: return methods;
15: }
16: }
17: }
Now let's go to Blend and create our popup. Select a Popup
control from the controls list and drop it anywhere on the form (better to put it on the object that it'll be binded to or, at least, on its parent layout element). You can put any single control on the popup, but, if you'd like to extend your functionality later, put some layout element. In my example, you can see that I've put a Grid
element. The ListBox
that will be populated with our methods is positioned on the Grid
. You have to set the following properties on the Popup
:
Placement="Bottom"
- This tells the system to position the Popup
at the bottom of the element it will be referring to. The same way as Visual Studio shows methods' intellisense below and to the right of the ".
" that you press.
StaysOpen ="False"
- This tells the WPF to close the Popup
if it loses focus.
IsOpen="False"
- This hides the Popup
on form initialization.
To make the ListBox
display method names correctly, you have to bind it to the ItemMethods
class that we've defined previously. Don't forget to compile your code - this will allow the Expression Blend to find it and help you with binding.
- Go to the Advanced Properties of the
ItemsSource
property (small rectangle to the right of the property value box) and click on Data Binding.
- Add the
ItemMethods
object from the list that will be presented to you after you press "+ CLR Object" button. This will add your new Data Source.
- On the Fields tree (on the right side), select the
Methods
collection.
If you did everything right - you'll see a list of SyndicationItem
methods in the Popup
. Note that Expression Blend has added a new data source for you...
1: <ObjectDataProvider x:Key="ItemMethodsDS"
2: d:IsDataSource="True"
3: ObjectType="{x:Type Castile:ItemMethods}"/>
... and added a bind source to your ListBox: ItemsSource="{Binding Path=Methods, Mode=OneWay, Source={StaticResource ItemMethodsDS}}"
.
I'm preparing a big article about data binding (it's a huge, powerful and extremely important part of WPF that you should now), but in a nutshell, this specific string
tells WPF to take the items from ItemMethodsDS
data source from a property Methods.
At this point, you're free to customize your ListBox
. For example, I've decided to remove the scrollbars (HorizontalScrollBarVisibility="Hidden"
and VerticalScrollBarVisibility="Hidden"
) and make the ListBox
searchable (when you start typing the letters, it'll jump to the item that starts with these letters).
1: <Popup x:Name="popupLinqMethods" Height="Auto" Width="150"
2: StaysOpen="False" Placement="Bottom" IsOpen="false"
3: d:LayoutOverrides="Width, Margin"
4: HorizontalAlignment="Left">
5: <Grid Width="Auto" Height="Auto">
6: <ListBox x:Name="lstMethodsSelection"
7: ScrollViewer.HorizontalScrollBarVisibility="Hidden"
8: ScrollViewer.VerticalScrollBarVisibility="Hidden"
9: KeyDown="OnMethodsSelectionKeyDown"
10: SelectedIndex="0"
11: IsTextSearchEnabled="True"
12: ItemsSource="{Binding Path=Methods, Mode=OneWay,
Source={StaticResource ItemMethodsDS}}"
13: ItemTemplate="{DynamicResource ListSyndicationObjectMethodsTemplate}"
14: />
15: </Grid>
16: </Popup>
To make the Popup
disappear when you press Enter (after selecting a desirable item) or when you press Escape, we need to let the ListBox
handle these events. Hence you need an event handler (note the KeyDown="OnMethodsSelectionKeyDown"
property in the XAML above). To do this, you have to put an event handler name in the KeyDown
event on the event's list of the ListBox
. After you'll press Enter, you'll be taken back to Visual Studio to edit your event handler. Here is how it can look like:
1: private void OnMethodsSelectionKeyDown
(object sender, System.Windows.Input.KeyEventArgs e)
2: {
3: switch (e.Key)
4: {
5: case System.Windows.Input.Key.Enter:
6:
7: popupLinqMethods.IsOpen = false;
8:
9: ListBox lb = sender as ListBox;
10: if (lb == null)
11: return;
12:
13:
14: string methodName = lb.SelectedItem.ToString();
15:
16:
17: int i = txtFilterText.CaretIndex;
18:
19:
20: txtFilterText.Text = txtFilterText.Text.Insert(i, methodName);
21:
22:
23: txtFilterText.CaretIndex = i + methodName.Length;
24:
25:
26: txtFilterText.Focus();
27: break;
28:
29: case System.Windows.Input.Key.Escape:
30:
31: popupLinqMethods.IsOpen = false;
32: break;
33: }
34: }
All what we did so far was the control of the behavior and the content of the Popup
, but we did nothing to trigger its appearance. Here your imagination is your limit. To make this sample work, I've decided to show the Popup
when the user will type the word item.
in the script editor. As soon as he'll press ".
" - the Popup will appear, allowing him to insert the selected method back to the script. The following KeyUp
text box event handler's code allows me to do just that. Note that the Key.OemPeriod
value is used to identify the pressed ".
" (dot). It wasn't that obvious to me. Note, as well, the hardcoded item.
hot-word. This is done to simplify the explanation. In your code, it should be modified to reflect your needs.
1: private void OnFilterTextKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
2: {
3: TextBox txtBox = sender as TextBox;
4: if ((txtBox == null) || (txtBox.CaretIndex == 0))
5: return;
6:
7:
8: if (e.Key != System.Windows.Input.Key.OemPeriod)
9: return;
10:
11:
12: string txt = txtBox.Text;
13: int wordStart = txt.LastIndexOf(' ', txtBox.CaretIndex - 1);
14: if (wordStart == -1)
15: wordStart = 0;
16:
17: string lastWord = txt.Substring(wordStart, txtBox.CaretIndex - wordStart);
18:
19:
20: if (lastWord.Trim().ToLower() != "item.")
21: return;
22:
23: ShowMethodsPopup(txtBox.GetRectFromCharacterIndex(txtBox.CaretIndex, true));
24: }
25:
26: private void ShowMethodsPopup(Rect placementRect)
27: {
28: popupLinqMethods.PlacementTarget = txtFilterText;
29: popupLinqMethods.PlacementRectangle = placementRect;
30: popupLinqMethods.IsOpen = true;
31: lstMethodsSelection.SelectedIndex = 0;
32: lstMethodsSelection.Focus();
33: }
It's a little tricky to place the Popup
exactly at the desired place - it has almost unlimited selection of different placement combinations (see MSDN help for the Placement
property. In my case I needed, as I've mentioned above, to open it to the lower right from the pressed ".
". So my Popup
has the Placement="Bottom"
- this will make it appear under the ".
".
How to find the ".
" location - you'll ask? That's a great question!
It's not that easy in Windows Forms, but was super easy in WPF. The character location in a TextBox
can be found by calling the GetrectFromCharacterIndex
method. But this will give you the coordinates of the character inside the TextBox
and the Popup
will open in the incorrect place, because it'll calculate its location relative to its parent Layout
element. This is not what we need. To compensate the calculation, we need to point the Popup PlacementTarget
to our TextBox
(see code above: PlacementTarget = txtFilterText
).
Now we're done! Start your script editor and start typing. You should see something very similar to the picture I've posted at the beginning of this article.
Points of Interest
History
- 9th January, 2008: Initial post
This is a single article from a series. Stay tuned.