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\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.