Caliburn: adding keyboard shortcuts

Posted by Siim on June 28th, 2012

We are creating a desktop app in WFP and using Caliburn.Micro. One thing we needed was to support keyboard shortcuts for almost all the interactions that user can do. Some of them are used globally and some of them depend on the focused control.

When talking about global shortcuts I mean that they can be used anywhere in the current view, they don’t depend on the element in focus. But they are not "globally" global, meaning that when I change the main view that is displayed in the window, shortcuts should be dependent on that view.

Caliburn uses cal:Message.Attach style of syntax to attach different actions for UI events, which can be defined in the XAML. It uses simple string parser to create correct bindings based on the content and it can be easily extended. I needed only to add custom syntax for my shortcuts, like ‘Shortcut Ctrl+F’. I override the parser for that:

public static class ShortcutParser
{
	public static bool CanParse(string triggerText)
	{
		return !string.IsNullOrWhiteSpace(triggerText) && triggerText.Contains("Shortcut");
	}

	public static TriggerBase CreateTrigger(string triggerText)
	{
		var triggerDetail = triggerText
			.Replace("[", string.Empty)
			.Replace("]", string.Empty)
			.Replace("Shortcut", string.Empty)
			.Trim();

		var modKeys = ModifierKeys.None;

		var allKeys = triggerDetail.Split('+');
		var key = (Key)Enum.Parse(typeof(Key), allKeys.Last());

		foreach (var modifierKey in allKeys.Take(allKeys.Count() - 1))
		{
			modKeys |= (ModifierKeys)Enum.Parse(typeof(ModifierKeys), modifierKey);
		}

		var keyBinding = new KeyBinding(new InputBindingTrigger(), key, modKeys);
		var trigger = new InputBindingTrigger { InputBinding = keyBinding };
		return trigger;
	}
}

And it’s attached to caliburn as follows:

var currentParser = Parser.CreateTrigger;
Parser.CreateTrigger = (target, triggerText) => ShortcutParser.CanParse(triggerText)
													? ShortcutParser.CreateTrigger(triggerText)
													: currentParser(target, triggerText);

 

The InputBindingTrigger class is pretty much the same as in this StackOverflow post. I had to modify the OnAttached method, because there are different paths when choosing the element to attach the shortcut.

If element is Focusable, then I can directly associate trigger with target element (like textbox, button etc). But when element isn’t focusable, it means the shortcut is meant to be used globally so I must attach it to the window. When attaching trigger to a window, it means that it is accessible anywhere in the app, whichever view is currently active.

We used the approach where there was only one active view in the conductor and because we needed global shortcuts per active view, I needed to add some code to remove the shortcut binding when the view is unloaded (some other view is activated). So here is revised OnAttached method:

protected override void OnAttached()
{
	if (InputBinding != null)
	{
		InputBinding.Command = this;
		if (AssociatedObject.Focusable)
		{
			AssociatedObject.InputBindings.Add(InputBinding);
		}
		else
		{
			Window window = null;
			AssociatedObject.Loaded += delegate
										{
											window = GetWindow(AssociatedObject);
											if (!window.InputBindings.Contains(InputBinding))
											{
												window.InputBindings.Add(InputBinding);
											}
										};
			AssociatedObject.Unloaded += delegate
											{
												window.InputBindings.Remove(InputBinding);
											};
		}
	}
	base.OnAttached();
}

This allows us to add shortcut binding in XAML with caliburn’s short-style syntax, like:

<UserControl cal:Message.Attach="[Shortcut F6] = [Action SomeAction];
				 [Shortcut F11] = [Action SomeAction2];
				 [Shortcut Ctrl+F] = [Action GlobalSearch]"></UserControl>

<TextBox cal:Message.Attach="[Shortcut Escape] = [Action Clear]" />

There’s only a one thing you need to be aware of. When attaching global shortcuts in caliburn’s way (remember, they are attached to the window), it means that it also uses guard methods to control the execution of the action. And when guard method prevents execution, caliburn by default disables the control associated with the action, in our case the whole window. Unfortunately I haven’t been able to find where to change that behavior, that it wouldn’t disable the control. Workaround is not to use guard methods with such cases and to rely on manual condition checking in actions.

WPF AutoCompleteBox–filtering similar items

Posted by Siim on May 30th, 2012

WPF toolkit (from Codeplex) includes a nice control for auto-complete textboxes. It supports also objects as items, not just strings. So we have a concept of selected item. Items can be filtered by simple string comparison (using simple built-in string based filters) or by defining custom filter which can use multiple properties (or whatever you like) to filter items. All these support XAML bindings so it’s all fine.

This allows us to use list of complex items as items source and use different properties for filtering, displaying items in drop-down list and displaying selected item in text box. For example, say we need to select a person from autocomplete and display only person’s last name in text box. But when user chooses person from autocomplete dropdown she wants to see person’s full name (and maybe some other properties) also so she can distinguish between persons. And lets say user wants to search by person’s nicknames also (meaning that when filtering results, we need to check person’s nicknames also). Okay, so far all good. We define custom ItemFilter and create binding for SelectedItem property and when we make a choice we can access selected item.

Problem occurs when there are multiple items with same display values (in our case multiple persons with the same last name). This triggers some weird behaviors on autocomplete side. We can see different persons in drop-down, make a selection and SelectedItem updates accordingly. But when drop-down closes, it looses currently selected item and starts finding it again from the items source based on the text in the text box. Because we have multiple persons with same last name, it selects first one it finds. And selected item is not what user chose, anymore.

Seems it’s quite well known problem based on google, but I didn’t find any solution that I really like. So it was time to dig into the source code. My idea was to create a separate property for my selected item (I call it RealSelectedItem), which behaves similarly to original SelectedItem, so it has also it’s own change events and so forth and I can directly bind to this property. Only problem is to find a proper place to update this property, so I can control when it’s updated and when not.

After digging through source, I found an interesting peace – ISelectionAdapter, which seems to be responsible for selection in drop-down only, sounds like a good place to start. It had events for commiting and cancelling selection, so I can get notifications when selection is changed. I overrode GetSelectionAdapterPart() method (which creates and returns ISelectionAdapter) and attached my methods to Commit and Cancel events when I update my RealSelectedItem property based on the SelectedItem. Full source here:

public class MyAutoCompleteBox : AutoCompleteBox
{
	public object SelectedRealItem
	{
		get { return GetValue(SelectedRealItemProperty); }
		set { SetValue(SelectedRealItemProperty, value); }
	}

	public static readonly DependencyProperty SelectedRealItemProperty =
		DependencyProperty.Register(
			&quot;SelectedRealItem&quot;,
			typeof(object),
			typeof(MyAutoCompleteBox),
			new PropertyMetadata(OnSelectedRealItemPropertyChanged));

	private static void OnSelectedRealItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
	{
		var source = d as MyAutoCompleteBox;
		source.SelectedItem = e.NewValue;
		if (e.NewValue == null)
		{
			source.Text = null;
		}

		var removed = new List&lt;object&gt;();
		if (e.OldValue != null)
		{
			removed.Add(e.OldValue);
		}

		var added = new List&lt;object&gt;();
		if (e.NewValue != null)
		{
			added.Add(e.NewValue);
		}

		source.OnRealSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
	}

	protected virtual void OnRealSelectionChanged(SelectionChangedEventArgs e)
	{
		RaiseEvent(e);
	}

	public static readonly RoutedEvent RealSelectionChangedEvent = EventManager.RegisterRoutedEvent(&quot;RealSelectionChanged&quot;, RoutingStrategy.Bubble, typeof(SelectionChangedEventHandler), typeof(MyAutoCompleteBox));

	public event SelectionChangedEventHandler RealSelectionChanged
	{
		add { AddHandler(RealSelectionChangedEvent, value); }
		remove { RemoveHandler(RealSelectionChangedEvent, value); }
	}

	protected override ISelectionAdapter GetSelectionAdapterPart()
	{
		var adapter = base.GetSelectionAdapterPart();
		adapter.Commit += (o, a) =&gt; UpdateSelectedValue(adapter.SelectedItem);
		adapter.Cancel += (o, a) =&gt; UpdateSelectedValue(null);
		return adapter;
	}

	private void UpdateSelectedValue(object value)
	{
		SelectedRealItem = value;
	}
}

Seems to work perfectly fine so far, with or without custom item template for drop down. Though, it doesn’t update RealSelectedItem when user navigates through items (like it is with SelectedItem), but I don’t need that anyway. I only want to know when user made up her mind and selection is made.

For Caliburn I needed to add my own element convention to use x:Name binding convention (actually it was already there, only for SelectedItem property), its basically one-liner (btw Caliburn doesn’t include convention for original SelectedItem either):

ConventionManager.AddElementConvention&lt;AsyncAutoCompleteBox&gt;(AsyncAutoCompleteBox.SelectedRealItemProperty, &quot;SelectedRealItem&quot;, &quot;RealSelectionChanged&quot;)
				.ApplyBinding = (viewModelType, path, property, element, convention) =&gt; ConventionManager.SetBinding(viewModelType, path, property, element, convention);

Copyright © 2007 Siim Viikman's blog.