Friday, October 9, 2009

Silverlight Behaviors and Triggers: Making a True Behavior

We have explored using dependency properties and attached properties to abstract behaviors and triggers. In my last article, TextBox Magic, I showed how to create a dependency property to enable a textbox filter that would prevent anything but digits. In this article, we'll take it a step further and turn it into a "true" behavior.

A "true" behavior inherits from Behavior. The first step is to add a reference to System.Windows.Interactivity. There are a number of nice things about using the behavior system that make them more flexible to use than basic dependency properties. First, the behaviors can integrate with Expression Blend. You can define a behavior, import it into the tool, and literally drag it onto a control to attach the behavior. Second, behaviors automatically provide overrides to tap into the attach/detatch events. Third, behaviors are typed to their target controls: you may have a generic behavior that targets FrameworkElement or a specific behavior that targets TextBox. Finally, we can decorate behaviors with attributes that are discovered by Intellisense and available when attaching the behavior.

We are going to create a filter behavior. The first step is to simply inherit from the base behavior class and then override the pertinent events:

public class TextBoxFilterBehavior : Behavior<TextBox>
{
   protected override void OnAttached()
   {
       AssociatedObject.KeyDown += _TextBoxFilterBehaviorKeyDown;
   }

   protected override void OnDetaching()
   {
       AssociatedObject.KeyDown -= _TextBoxFilterBehaviorKeyDown; 
   }
}

As you can see, we have a convenient place to hook into the KeyDown event and unhook at the end. We also have a nice, typed AssociatedObject to use and manipulate.

We'll extend our filter to handle alpha (alphanumeric and spaces), positive numeric digits, and regular numeric digits. For the regular numeric digits, we'll allow the subtract/minus sign only if it's the very first character in the text. The "helper" methods look like this:

private static readonly List _controlKeys = new List<Key>
                                                     {
                                                         Key.Back,
                                                         Key.CapsLock,
                                                         Key.Ctrl,
                                                         Key.Down,
                                                         Key.End,
                                                         Key.Enter,
                                                         Key.Escape,
                                                         Key.Home,
                                                         Key.Insert,
                                                         Key.Left,
                                                         Key.PageDown,
                                                         Key.PageUp,
                                                         Key.Right,
                                                         Key.Shift,
                                                         Key.Tab,
                                                         Key.Up
                                                     };

private static bool _IsDigit(Key key)
{
    bool shiftKey = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
    bool retVal;
    if (key >= Key.D0 && key <= Key.D9 && !shiftKey)
    {
        retVal = true;
    }
    else
    {
        retVal = key >= Key.NumPad0 && key <= Key.NumPad9;
    }
    return retVal;
}

static bool _HandleNumeric(string text, Key key)
{
    bool handled = true;
    
    // if empty, allow minus - only can be first character
    if (string.IsNullOrEmpty(text.Trim()) && key.Equals(Key.Subtract))
    {
        handled = false;
    }
    else if (_controlKeys.Contains(key) || _IsDigit(key))
    {
        handled = false;
    }

    return handled;
}

static bool _HandlePositiveNumeric(Key key)
{
    bool handled = true;

    if (_controlKeys.Contains(key) || _IsDigit(key))
    {
        handled = false;
    }

    return handled;
}

static bool _HandleAlpha(Key key)
{
    bool handled = true;

    if (_controlKeys.Contains(key) || _IsDigit(key) || key.Equals(Key.Space) ||
        (key >= Key.A && key <= Key.Z))
    {
        handled = false;
    }

    return handled;
}

public enum TextBoxFilterType
{
    AlphaFilter,
    PositiveNumericFilter,
    NumericFilter
}

public TextBoxFilterType FilterType { get; set; }

As you can see, the familiar list of "friendly" keys is carried over. There are some helper methods for our different types of filters, as well as an enumeration. The enumeration gives us the flexibility of determining which filter to use and then a property is exposed to set that enumeration.

The last step is to wire in the actual event:

void _TextBoxFilterBehaviorKeyDown(object sender, KeyEventArgs e)
{
    switch(FilterType)
    {
        case TextBoxFilterType.AlphaFilter:
            e.Handled = _HandleAlpha(e.Key);
            break;

        case TextBoxFilterType.NumericFilter:
            e.Handled = _HandleNumeric(AssociatedObject.Text, e.Key);
            break;

        case TextBoxFilterType.PositiveNumericFilter:
            e.Handled = _HandlePositiveNumeric(e.Key);
            break;
    }
}

As you can see, it's as simple as checking the enumeration, calling the helper function and modifying the handled property.

Now we can reference the project with our behavior and reference the System.Windows.Interactivity. In XAML, you attach the behavior like this:

<TextBox>
   <Interactivity:Interaction.Behaviors>
      <Behaviors:TextBoxFilterBehavior FilterType="AlphaFilter"/>
   </Interactivity:Interaction.Behaviors>
</TextBox>

As you can see, easy to add, easy to read quickly what the behavior does, and the added bonus is that you can manipulate these in Expression Blend.

Jeremy Likness

Edit 10/16/2009: Thanks to all who pointed out the bug in the detach event ... it's been fixed.

1 comment:

  1. Don't mean to nitpick, but the OnDetaching() should read:

    AssociatedObject.KeyDown -= _TextBoxFilterBehaviorKeyDown;

    ReplyDelete