Windows 8 XAML Tips - Creating Blend Behaviors

By Fons Sonnemans, posted on
6650 Views

The new version of Blend for Visual Studio 2013 RC now supports Behaviors. I have used Behaviors for developing Silverlight and Windows Phone applications a lot. Now you can use them for you Windows Store XAML apps too. Two type of Behaviors are supported: Behaviors and Actions. Triggers are "dropped" and can/should now be implemented using Behaviors. 

There are 7 build-in Actions: CallMethodAction, ChangePropertyAction, ControlStoryboardAction, GoToStateAction, InvokeCommandAction, NavigateToPageAction and PlaySoundAction. Many were already available in Silverlight. The NavigateToPageAction is new.

There are 3 build-in Behaviors: DataTriggerBehavior, EventTriggerBehavior and IncrementalUpdateBehavior.

You can create your own Actions and Behaviors.  Before you can do that you must add a reference to the 'Behaviors SDK'.

Creating Actions

You create an Action by adding a class to your project which derives from the DependencyObject class and implements the IAction interface. In Silverlight there was a TriggerAction base class, but that no longer exists. The IAction interface has only one Execute() method you should implement.

As an example I have created a simple ShowMessageAction. It contains a Text dependency property which you can set or databind. The Execute() method will show this Text in a MessageDialog.

using Microsoft.Xaml.Interactivity;
using System;
using Windows.UI.Popups;
using Windows.UI.Xaml;

namespace BehaviorsDemo.Behaviors {
    
    public class ShowMessageAction : DependencyObject, IAction {

        #region Text Dependency Property

        /// <summary> 
        /// Get or Sets the Text dependency property.  
        /// </summary> 
        public string Text {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        /// <summary> 
        /// Identifies the Text dependency property. This enables animation, styling, 
        /// binding, etc...
        /// </summary> 
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text",
                                        typeof(string),
                                        typeof(ShowMessageAction),
                                        new PropertyMetadata(string.Empty));

        

        #endregion Text Dependency Property

        public object Execute(object sender, object parameter) {
#pragma warning disable 4014
            new MessageDialog(this.Text).ShowAsync();
#pragma warning restore
            return null;
        }
    }
}

Use ShowMessageAction

After you have compiled the application you should switch to Blend to add this behavior to a control. I my case I have created a simple page containing a TextBox and a Button. In the 'Assets' window you should select the 'Behaviors'. You should see the 'ShowMessageAction' at the bottom of the list. Drag this action and drop it on the Buttton. The Text property of the action is databound to the Text property of the TextBox.

The ShowMessageAction is placed in an EventTriggerBehavior which has its EventName set to 'Click'. You can of course select a different event if you want. You can also select a different SourceObject. This is the object whose event is handled to trigger the action. The EventTriggerBehavior replaces the EventTrigger of Silverlight.

The XAML of this page.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:BehaviorsDemo"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
      xmlns:Behaviors="using:BehaviorsDemo.Behaviors"
      x:Class="BehaviorsDemo.MainPage"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          DataContext="{Binding Source={StaticResource SampleDataSource}}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="85*" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="textBoxDemo"
                     HorizontalAlignment="Left"
                     TextWrapping="Wrap"
                     Text="Hello World"
                     Width="400" />
            <Button Content="Show"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Top">
                <Interactivity:Interaction.Behaviors>
                    <Core:EventTriggerBehavior EventName="Click">
                        <Behaviors:ShowMessageAction Text="{Binding Text, ElementName=textBoxDemo}" />
                    </Core:EventTriggerBehavior>
                </Interactivity:Interaction.Behaviors>
            </Button>
        </StackPanel>
    </Grid>
</Page>

When you run the app and click the Button the text of the TextBox is shown in a MessageDialog. I think this is very cool. Designers can now add interactivity to the app without writing code.

Creating Behaviors

3 years ago I wrote this blog post Keyboard selection on Silverlight ListBox and ComboBox. It describes a Behavior which I used in my Silverlight apps. You could drop the Behavior on a ListBox or ComboBox. This enabled keyboard selection. So, if you pressed the key 'X', the ListBox/ComboBox would select the first row which started with the letter X. I always found this Behavior very userful. So I converted it to the new Windows 8.1 Behaviors. This allows me to use it for the Windows 8 DataControls (GridView, ListView and FlipView) without any extra programming effort.

You create a Behavior by adding a class to your project which derives from the DependencyObject class and implements the IBehavior interface. In Silverlight there was a Behavior and Behavior<T> base class, but that no longer exists. To make my conversion easier I re-created the Behavior<T> base class. It is an abstract class with an AssociatedObject (oftype T) property and two virtual methods OnAttached() and OnDetatching().

using Microsoft.Xaml.Interactivity;
using System;
using Windows.UI.Xaml;

namespace BehaviorsDemo.Behaviors {

    public abstract class Behavior<T> : DependencyObject, IBehavior where T : DependencyObject {

        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
        public T AssociatedObject { get; set; }

        protected virtual void OnAttached() {
        }

        protected virtual void OnDetaching() {
        }

        public void Attach(Windows.UI.Xaml.DependencyObject associatedObject) {
            this.AssociatedObject = (T)associatedObject;
            OnAttached();
        }

        public void Detach() {
            OnDetaching();
        }

        DependencyObject IBehavior.AssociatedObject {
            get { return this.AssociatedObject; }
        }
    }
}

The KeyboardSelectionBehavior class is almost a one-on-one port of my Silverlight code. I just changed some namespaces and cleaned up some code. The ListBoxItem is now replaced by a SelectorItem.

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;

namespace BehaviorsDemo.Behaviors {

    /// <summary>
    /// This behavior can be attached to a ListBox or ComboBox to 
    /// add keyboard selection
    /// </summary>
    public class KeyboardSelectionBehavior : Behavior<Selector> {

        private bool _boundToComboBoxItemsPanelRoot;

        /// <summary>
        /// Gets or sets the Path used to select the text
        /// </summary>
        public string SelectionMemberPath { get; set; }

        /// <summary>
        /// Attaches to the specified object: subscribe on KeyDown event
        /// </summary>
        protected override void OnAttached() {
            base.OnAttached();
            this.AssociatedObject.KeyDown += DoKeyDown;

            // subscribe on KeyDown event of the ItemsPanelRoot
            // of a ComboBox when it is opened
            var cb = this.AssociatedObject as ComboBox;
            if (cb != null) {
                cb.DropDownOpened += ComboBox_DropDownOpened;
            }
        }

        /// <summary>
        /// subscribe on KeyDown event of ItemsPanelRoot of a ComboBox
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ComboBox_DropDownOpened(object sender, object e) {
            var itemsPanelRoot = this.AssociatedObject.ItemsPanelRoot;
            if (_boundToComboBoxItemsPanelRoot == false && itemsPanelRoot != null) {
                _boundToComboBoxItemsPanelRoot = true;
                itemsPanelRoot.KeyDown += DoKeyDown;
            }
        }

        /// <summary>
        /// Detaches to the specified object: Unsubscribe on KeyDown event(s)
        /// </summary>
        protected override void OnDetaching() {
            this.AssociatedObject.KeyDown -= DoKeyDown;
            
            var cb = this.AssociatedObject as ComboBox;
            if (cb != null) {
                cb.DropDownOpened -= ComboBox_DropDownOpened;
                if (_boundToComboBoxItemsPanelRoot) {
                    var itemsPanelRoot = this.AssociatedObject.ItemsPanelRoot;
                    if (itemsPanelRoot != null) {
                        _boundToComboBoxItemsPanelRoot = false;
                        itemsPanelRoot.KeyDown -= DoKeyDown;
                    }
                }
            }
            base.OnDetaching();
        }

        /// <summary>
        /// Select the correct item
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void DoKeyDown(object sender, KeyRoutedEventArgs e) {
            // Create a list of strings and indexes
            int index = 0;
            IEnumerable<Item> list = null;

            var path = SelectionMemberPath ??
                this.AssociatedObject.DisplayMemberPath;

            var evaluator = new BindingEvaluator();
            if (path != null) {
                list = this.AssociatedObject.Items.OfType<object>()
                    .Select(item => {
                        // retrieve the value using the supplied Path
                        var binding = new Binding();
                        binding.Path = new PropertyPath(path);
                        binding.Source = item;

                        BindingOperations.SetBinding(evaluator,
                            BindingEvaluator.TargetProperty, binding);
                        var value = evaluator.Target;

                        return new Item {
                            Index = index++,
                            Text = Convert.ToString(value)
                        };
                    });
            } else {
                list = this.AssociatedObject.Items.OfType<SelectorItem>()
                    .Select(item => new Item {
                        Index = index++,
                        Text = Convert.ToString(item.Content)
                    });
            }
            // Sort the list starting at next selectedIndex to the end and 
            // then from the beginning
            list = list.OrderBy(item => item.Index <=
                this.AssociatedObject.SelectedIndex ?
                item.Index + this.AssociatedObject.Items.Count : item.Index);

            // Find first starting with 
            var text = e.Key.ToString();
            var first = list.FirstOrDefault(item => item.Text.StartsWith(text,
                StringComparison.CurrentCultureIgnoreCase));

            if (first != null) {
                // found
                this.AssociatedObject.SelectedIndex = first.Index;
            }
        }

        /// <summary>
        /// Helper class
        /// </summary>
        private class Item {
            public int Index { get; set; }
            public string Text { get; set; }
        }

        /// <summary>
        /// Helper class used for property path value retrieval
        /// </summary>
        private class BindingEvaluator : FrameworkElement {

            public static readonly DependencyProperty TargetProperty =
                DependencyProperty.Register(
                    "Target",
                    typeof(object),
                    typeof(BindingEvaluator), null);

            public object Target {
                get { return GetValue(TargetProperty); }
                set { SetValue(TargetProperty, value); }
            }

        }
    }
}

Use KeyboardSelectionBehavior

To demonstrate the KeyboardSelectionBehavior I added some sample data containing Employees to the Project. An Employee has a Name, Salary and Image property. I created a GridView in which the ItemSource is data bound to the Employees sample data.

Next, I dragged the KeyboardSelectionBehavior from the Assets Window and dropped it on the GridView.

The behavior has a SelectionMemberPath property to specify which property to use for selection. In this example it is set to the Name of the databound Employee.

The XAML of this page.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:BehaviorsDemo"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
      xmlns:Behaviors="using:BehaviorsDemo.Behaviors"
      x:Class="BehaviorsDemo.MainPage"
      mc:Ignorable="d">
    <Page.Resources>
        <DataTemplate x:Key="EmployeesItemTemplate">
            <Grid Height="110"
                  Width="480"
                  Margin="10">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"
                        Width="110"
                        Height="110">
                    <Image Source="{Binding Image}"
                           Height="110"
                           Width="110" />
                </Border>
                <StackPanel Grid.Column="1"
                            Margin="10,0,0,0">
                    <TextBlock Text="{Binding Name}"
                               Style="{StaticResource TitleTextBlockStyle}" />
                    <TextBlock Text="{Binding Salary}"
                               Style="{StaticResource CaptionTextBlockStyle}"
                               TextWrapping="NoWrap" />
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          DataContext="{Binding Source={StaticResource SampleDataSource}}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="85*" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="textBoxDemo"
                     HorizontalAlignment="Left"
                     TextWrapping="Wrap"
                     Text="Hello World"
                     Width="400" />
            <Button Content="Show"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Top">
                <Interactivity:Interaction.Behaviors>
                    <Core:EventTriggerBehavior EventName="Click">
                        <Behaviors:ShowMessageAction Text="{Binding Text, ElementName=textBoxDemo}" />
                    </Core:EventTriggerBehavior>
                </Interactivity:Interaction.Behaviors>
            </Button>

        </StackPanel>
        <GridView ItemTemplate="{StaticResource EmployeesItemTemplate}"
                  ItemsSource="{Binding Employees}"
                  IsSwipeEnabled="False"
                  Grid.Row="1"
                  SelectionMode="Single">
            <Interactivity:Interaction.Behaviors>
                <Behaviors:KeyboardSelectionBehavior SelectionMemberPath="Name" />
            </Interactivity:Interaction.Behaviors>
        </GridView>

    </Grid>
</Page>

Run the app and focus the GridView. You can then press the key 'C' when and it will select 'Carpenter, Terry' employee.

Closure and download

I hope you like my solution. Maybe you can use it in your apps too. You can download my code below. My colleague MVP Timmy Kokke has created the https://www.blendbehaviors.net/ website which is used to share Behaviors and Actions. You will soon be able to download my ShowMessageAction and KeyBoardSelectionBehavior there as well.

Cheers,

Fons

Download

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.

Leave a comment

Blog comments

0 responses