Tuesday, November 2, 2010

.NET 3.5 WPF Triggers - KeyDownTrigger

After having used Silverlight 4.0, i found myself getting frustrated using Silverlight 3.0, or even WPF of any version (lets face it, WPF just doesn't get the same loving the Silverlight does from the Microsoft Development Team) because of the absence of certain triggers and behaviors.

Since some of our projects rely on collaboration with people who don't have licenses to VS2010, we've had to keep those projects using .NET Framework 3.5. I suppose I ask for too much...but i always want more! And since the triggers and behaviors i wanted didn't work in 3.5, i decided to make more! Here's my code for the KeyDownTrigger:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Interactivity;
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;

namespace ToP.Common.Triggers
{
    [DesignerCategory("Triggers")]
    public class KeyDownTrigger : TriggerBase<FrameworkElement>
    {

        /// <summary>
        /// Overides the OnAttached Method and adds our subscription to the PreviewKeyDown Event
        /// </summary>
        protected override void OnAttached ()
        {
            base.OnAttached();
            AssociatedObject.PreviewKeyDown += AssociatedObject_KeyDown;
        }

        /// <summary>
        /// Overides the OnDetaching Method and removes our subscription to the PreviewKeyDown Event
        /// </summary>
        protected override void OnDetaching ()
        {
            base.OnDetaching();
            AssociatedObject.PreviewKeyDown -= AssociatedObject_KeyDown;
        }

        [Category("KeyEvent Properties"), Description("Comma Delimitered list of Keys the trigger is contigent on")]
        public string Keys { 
            get
            {
                return (string)GetValue(KeysProperty);
            }
            set
            {
                SetValue(KeysProperty, value);
            }
        }
        public static readonly DependencyProperty KeysProperty =
            DependencyProperty.Register("Keys", typeof(string), typeof(KeyDownTrigger), new PropertyMetadata("None"));
        private List<Key> KeyList
        {
            get
            {
                return Keys.Split(',').Select(x => (Key)Enum.Parse(typeof(Key), x)).ToList();
            }
        }
        private List<ModifierKeys> ModifierList
        {
            get
            {
                return ModifierKeys.Split(',').Select(x => string.IsNullOrEmpty(x) ? System.Windows.Input.ModifierKeys.None : (ModifierKeys)Enum.Parse(typeof(ModifierKeys), x)).ToList();
            }
        }

        [Category("KeyEvent Properties"), Description("Comma Delimitered list of ModifierKeys the trigger is contigent on")]
        public string ModifierKeys
        {
            get
            {
                return (string)GetValue(ModifierKeysProperty);
            }
            set
            {
                SetValue(ModifierKeysProperty, value);
            }
        }
        public static readonly DependencyProperty ModifierKeysProperty =
            DependencyProperty.Register("ModifierKeys", typeof(string), typeof(KeyDownTrigger), new PropertyMetadata("None"));

        [Category("KeyEvent Properties"), Description("Sets the value of e.Handled for the PreviewKeyDown event. Default value is 'True'")]        
        public bool HandleKey
        {
            get
            {
                return (bool)GetValue(HandleKeyProperty);
            }
            set
            {
                SetValue(HandleKeyProperty, value);
            }
        }
        public static readonly DependencyProperty HandleKeyProperty =
            DependencyProperty.Register("HandleKey", typeof(bool), typeof(KeyDownTrigger), new PropertyMetadata(true));


        /// <summary>
        /// CallBack event to the PreviewKeyDown subscription that will InvokeActions 
        /// If the Key & Modifiers are found in our lists
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AssociatedObject_KeyDown (object sender, KeyEventArgs e)
        {
            if (KeyList.Contains(e.Key))
            {
                if (ModifierList.Count > 0)
                {
                    if (ModifierList.Contains(Keyboard.Modifiers))
                    {
                        InvokeActions(new TriggerKeys { Sender = AssociatedObject, Keys = e.Key, ModifierKeys = Keyboard.Modifiers });
                        e.Handled = HandleKey;
                    }
                }
                else
                {
                    InvokeActions(new TriggerKeys { Sender = AssociatedObject, Keys = e.Key, ModifierKeys = Keyboard.Modifiers });
                    e.Handled = HandleKey;
                }
            }
        }
    }

    public class TriggerKeys
    {
        public object Sender { get; set; }
        public Key Keys { get;  set; }
        public ModifierKeys ModifierKeys { get; set; }
    }
}

I was asked why i was using DependencyProperty(s), and it was simply to allow for those values to be databound to something. You'll also notice in the above code that we subscribe and unsubscribe to the Event. That can be done for ANY event, so you can use this idea to subscribe to any event you'd like!

This Trigger allows you to specify Keys, as well as ModifierKeys. It will also allow you to specifiy whether or not you want it to stop the RoutedEvent bubble after being triggered.

..And best yet, it works in both WPF & Silverlight, tested in .NET Framework 3.5 and .NET Framework 3.0!

Assuming we declare the namespace in the XAML as:
xmlns:triggers="clr-namespace:ToP.Common.Triggers"
The usage will be as follows:
<triggers:KeyDownTrigger HandleKey="True" Keys="Up" ModifierKeys="Control">
     <actions:FocusTarget TargetName="txtFirstName" />
/>
    </triggers:KeyDownTrigger>

Enjoy! I'll be posting my other Triggers shortly!

2 comments :

Slow Walker said...

Nice one. :)

Adam Cloud said...

Thank you! I had an "aha!" moment when i first started getting it down. I hate having to change my programming style just because i'm collaborating with someone else :P