I wrote a Hotkey-System for command-based WPF applications. Could you please take a look at my code (even if it consists of multiple classes :P). I am mainly interested in following things:
- How readable is my code?
- How well or bad is this programmed and designed?
- Are there flaws in the design which could prevent this code to be integrated easily in other WPF applications?
- What is your opinion about the coding style?
But I am looking forward for any other suggestions from your side!
The main idea is to have hotkeys linked to commands to e.g. open HelpWindow with the key F1 or to rotate a certain object with Ctrl+MouseWheel without hard-coding the shortcuts.
A command has as many Hotkey members as needed, which get bind e.g. in the .xaml of a window.
The combination of a hotkey is declared in a settings-file, which gets embedded into the app.config. So the shortcuts for the commands are editable outside the application and without the need of recompiling it.
A hotkey consist of the type of the linked command, an InputGesture, a description and a display-string (e.g. "Ctrl+C"; could be used for localization as well).
This an example how it looks like in the .config-file:
<MapEditor.Properties.HotKeySettings>
<setting name="ExitCommandKeyHotKey" serializeAs="String">
<value>Alt+F4</value>
</setting>
</MapEditor.Properties.HotKeySettings>
This is how it looks like in the command:
public class ExitCommand : CommandBase
{
private KeyHotKey m_KeyHotKey;
public KeyHotKey KeyHotKey
{
get { return m_KeyHotKey; }
set { m_KeyHotKey = value; }
}
public ExitCommand()
{
m_KeyHotKey = MapEditorHotKeyProvider.Instance.GetHotKeyOrDefault<KeyHotKey>(this.GetType());
}
public override bool CanExecute(object parameter)
{
return true;
}
public override void Execute(object parameter)
{
Application.Current.MainWindow.Close();
Application.Current.Dispatcher.InvokeShutdown();
}
}
This is an example for a binding in the shell/main-windows .xaml:
<Window.InputBindings>
<KeyBinding Modifiers="{Binding ExitApplication.KeyHotKey.KeyGesture.Modifiers}" Key="{Binding ExitApplication.KeyHotKey.KeyGesture.Key}" Command="{Binding ExitApplication}" />
</Window.InputBindings>
The idea of the underlying system is to have a Provider-class (HotKeyProvider) which provides and saves all declared hotkeys.
When a command requests a hotkey the provider checks if there is already this hotkey, then it's returned, otherwise it tries to create it.
For the creation of a hotkey it is necessary to derive from HotKeyProvider to create a specific method for creating hotkeys.
But take a look at the code:
HotKeyProvider (Provides, saves and creates hotkeys, caution I am using log4net here)
using log4net;
using System;
using System.Linq;
namespace HotKeySystem
{
//Provider class for creating, storing and providing HotKeys
public abstract class HotKeyProvider
{
//Enum to specify LoggerWarnMessage
protected enum DefaultReturnTypes { NULL = 0, DEFAULT_GESTURE };
protected HotKeyList<HotKeyBase> m_HotKeys = new HotKeyList<HotKeyBase>();
public virtual HotKeyList<HotKeyBase> HotKeys
{
get { return m_HotKeys; }
}
//Returns exisiting hotkey, otherwise tries to create new one
public virtual T GetHotKey<T>(Type commandType)
where T : HotKeyBase, new()
{
var hotkey = m_HotKeys.FirstOrDefault<HotKeyBase>(item => item.CommandType.Equals(commandType) && item is T)
?? CreateHotKey<T>(commandType);
if(hotkey == null)
{
throw new ArgumentNullException("HotKeyProvider - GetHotKey<"
+ typeof(T).FullName + ">(" + commandType.FullName
+ "): Fetching hotkey from internal List or trying to create it resulted in null argument!");
}
return hotkey as T;
}
//Returns exisiting hotkey, otherwise tries to create new one or returns default-value null
public virtual T GetHotKeyOrNull<T>(Type commandType)
where T : HotKeyBase, new()
{
return TryGetHotKey<T>(commandType, DefaultReturnTypes.NULL);
}
//Returns exisiting hotkey, otherwise tries to create new one or returns default-gesture 'None'
//A 'default-gesutre 'None'' is a HotKey object containing an InputGesture with a Key/MouseAction 'None' (take a look at the default-constructors of the HotKeys), so it's a valid HotKey/Gesture but won't fire in any case
public virtual T GetHotKeyOrDefault<T>(Type commandType)
where T : HotKeyBase, new()
{
return TryGetHotKey<T>(commandType, DefaultReturnTypes.DEFAULT_GESTURE) ?? new T();
}
protected virtual T TryGetHotKey<T>(Type commandType, DefaultReturnTypes defaultReturnType)
where T : HotKeyBase, new()
{
T hotkey = null;
try
{
hotkey = GetHotKey<T>(commandType);
}
catch(Exception e)
{
LogManager.GetLogger("HotKeyProvider").Warn(
GetWarnMessage("TryGetHotKey", commandType, typeof(T), e.Message)
+ "\n"
+ GetDefaultTypeMessage(defaultReturnType));
}
return hotkey;
}
protected virtual string GetWarnMessage(string subMethodName, Type commandType, Type hotkeyType, string ExceptionMessage)
{
return "HotKeyProvider - " + subMethodName + ": Could not create HotKey of Type " + hotkeyType.FullName
+ " and for CommandType: " + commandType.FullName
+ "\nInnerExceptionMessage: " + ExceptionMessage;
}
protected virtual string GetDefaultTypeMessage(DefaultReturnTypes defaultReturnType)
{
var defaultTypeMessage = "Returning ";
//For more specified log-message
switch (defaultReturnType)
{
case DefaultReturnTypes.NULL:
defaultTypeMessage += "null";
break;
case DefaultReturnTypes.DEFAULT_GESTURE:
defaultTypeMessage += "default('none')-gesture";
break;
default:
defaultTypeMessage += defaultReturnType.ToString();
break;
}
return defaultTypeMessage;
}
protected abstract T CreateHotKey<T>(Type commandType) where T : HotKeyBase, new();
}
}
MapEditorHotKeyProvider (specific version)
using Wpf.Utility.HotKeySystem;
using System;
namespace MapEditor.Services
{
//HotKeyProvider for MapEditor
public class MapEditorHotKeyProvider : HotKeyProvider
{
//Singleton-Pattern based on MapInputTracker
#region Singleton
private MapEditorHotKeyProvider()
{
}
public static MapEditorHotKeyProvider Instance
{
get { return Nested.Instance; }
}
// ReSharper disable ClassNeverInstantiated.Local
private class Nested
{
internal static readonly MapEditorHotKeyProvider Instance = new MapEditorHotKeyProvider();
}
#endregion
protected override T CreateHotKey<T>(Type commandType)
{
var commandName = commandType.Name;
var typeName = typeof(T).Name;
//Fetch hotkey data from resource and config files
var description = Properties.HotKeysDescriptions.ResourceManager.GetString(commandName) ?? String.Empty;
string hotkeyGestureString = null;
try
{
hotkeyGestureString = Properties.HotKeySettings.Default[commandName + typeName] as string;
}
catch(Exception)
{
//The exception is just a simple derived class from exception
throw new HotKeyNotDeclaredException("MapEditorHotKeyProvider - CreateHotKey<"
+ typeName + ">(" + commandName
+ "): Requested HotKey is not declared in HotKeySettings/App.config!");
}
//Create new hotkey
var newHotKey = HotKeyAdapter.GetNewHotKey<T>(new HotKeyConfig(commandType, hotkeyGestureString, description));
if(newHotKey != null)
{
//Localize DisplayString of new hotkey
//The localizer should not be object of interest here
newHotKey.HotKeyDisplayString = HotKeyLocalizer.LocalizeDisplayString(newHotKey.HotKeyDisplayString);
m_HotKeys.Add(newHotKey);
}
return newHotKey;
}
}
}
HotKeyAdapter ("basic" adapter to provide correct HotKey-objects)
using System;
namespace HotKeySystem
{
//Adapter-class for HotKeys to provide HotKey-object of specific type
public static class HotKeyAdapter
{
public static HotKeyBase GetNewHotKey(Type hotkeyType, HotKeyConfig config)
{
HotKeyBase hotkey = null;
if (hotkeyType.Equals(typeof(KeyHotKey)))
{
hotkey = new KeyHotKey(config);
}
else if (hotkeyType.Equals(typeof(MouseHotKey)))
{
hotkey = new MouseHotKey(config);
}
else
{
throw new NotImplementedException("HotKeyAdapter - GetNewHotKey: Could not provide requested HotKey of Type: " + hotkeyType.FullName);
}
return hotkey;
}
public static T GetNewHotKey<T>(HotKeyConfig config)
where T : HotKeyBase, new()
{
return Activator.CreateInstance(typeof(T), config) as T;
}
}
}
HotKeyBase (Base class for the hotkeys)
using System;
using System.Windows.Input;
namespace Wpf.Utility.HotKeySystem
{
//Base class for a HotKey, which is linked to a specific command
public abstract class HotKeyBase
{
//Type of the linked command
public virtual Type CommandType { get; set; }
//Displayed shortcut e.g.: 'Ctrl+A' or 'Umschalttaste+Linksklick'
public virtual string HotKeyDisplayString { get; set; }
//Description what the HotKeyNEW does e.g.: 'Close current project'
public virtual string Description { get; set; }
//Creates an Gesture from a String
public virtual T CreateGestureFromString<T>(string gestureString)
where T : InputGesture
{
//The GestureConverterAdapter is just a "basic" Adapter which returns the correct Converter (based on the TypeConverter class)
return GestureConverterAdapter.GetGestureConverter(typeof(T)).ConvertFromString(gestureString) as T;
}
}
}
KeyHotKey
using System;
using System.Globalization;
using System.Threading;
using System.Windows.Input;
namespace Wpf.Utility.HotKeySystem
{
//HotKey-class for a KeyGesture-HotKey
public class KeyHotKey : HotKeyBase
{
//Converter to convert strings to KeyGestures
protected static readonly KeyGestureConverter s_KeyConverter = new KeyGestureConverter();
public KeyHotKey()
{
KeyGesture = new KeyGesture(Key.None);
}
public KeyHotKey(HotKeyConfig config)
: this(config.CommandType, config.hotkeyGestureString, config.Description)
{
}
public KeyHotKey(Type commandType, string hotkeyGestureString, string description)
{
CommandType = commandType;
Description = description;
KeyGesture = CreateGestureFromString<KeyGesture>(hotkeyGestureString);
HotKeyDisplayString = GetDisplayStringFromGesture(KeyGesture);
}
public KeyHotKey(Type commandType, KeyGesture keyGesture, string description)
{
CommandType = commandType;
Description = description;
KeyGesture = keyGesture;
HotKeyDisplayString = GetDisplayStringFromGesture(KeyGesture);
}
public virtual KeyGesture KeyGesture { get; set; }
public virtual string GetDisplayStringFromGesture(KeyGesture gesture, CultureInfo cultureInfo = null)
{
return gesture.GetDisplayStringForCulture(cultureInfo ?? Thread.CurrentThread.CurrentUICulture);
}
}
}
MouseHotKey
using System;
using System.Windows.Input;
namespace Wpf.Utility.HotKeySystem
{
//HotKey-class for a MouseGesture-HotKey
public class MouseHotKey : HotKeyBase
{
protected static readonly MouseGestureConverter s_MouseConverter = new MouseGestureConverter();
protected MouseGesture m_MouseGesture;
public MouseHotKey()
{
new MouseGesture(MouseAction.None);
}
public MouseHotKey(HotKeyConfig config)
: this(config.CommandType, config.hotkeyGestureString, config.Description)
{
}
public MouseHotKey(Type commandType, string hotkeyGestureString, string description)
{
CommandType = commandType;
Description = description;
MouseGesture = CreateGestureFromString<MouseGesture>(hotkeyGestureString);
HotKeyDisplayString = GetDisplayStringFromGesture(MouseGesture);
}
public MouseHotKey(Type commandType, MouseGesture mouseGesture, string description)
{
CommandType = commandType;
Description = description;
MouseGesture = mouseGesture;
HotKeyDisplayString = GetDisplayStringFromGesture(m_MouseGesture);
}
public virtual MouseGesture MouseGesture { get; set; }
public virtual string GetDisplayStringFromGesture(MouseGesture gesture)
{
//There is no build-in "GetDisplayString"-Method, so the displayString needs to be composed
var displayString = gesture.MouseAction.ToString();
if(!gesture.Modifiers.ToString().Equals(ModifierKeys.None))
{
displayString = gesture.Modifiers.ToString() + "+" + displayString;
}
return displayString;
}
}
}