2
\$\begingroup\$

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;
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.