Metro XAML Nugget: App Bar AutoMagic

You may have noticed that in many places of the Windows 8 Metro UI, as well as many Metro applications where list content can be selected, that making a selection automatically/magically (“automagically”) brings up one or more app bars.  This is consistent with the “Guidelines and checklist for app bars” published in the Metro Style Apps Dev Center:

Do place contextual commands on an app bar and show that bar programmatically.

If you have commands that are specific to the mode of the app, such as the Crop command appearing only when a photo is selected, place those commands on an app bar and show it programmatically while in that context.

If you have commands that can be performed whether content is selected or not, keep those commands on the bar if you have enough room.

Do set the app bar’s dismissal mode to sticky when displaying contextual commands.

If you have contextual commands on an app bar, set the mode to sticky while that context exists and turn off the sticky mode when the context is no longer present (such as when a photo is deselected). In sticky mode, the bar doesn’t automatically hide when the user interacts with the app. This is useful for multi-select scenarios or when the context involves interaction such as manipulating crop handles. The bar stays visible while the user performs their actions. The user can still hide the bar by swiping the top or bottom edge of the screen and they can show it again with an edge swipe.

One place where this behavior can be seen occurs when selecting and deselecting tiles in the Metro Start Screen.

image

Of course, this is nice and all, but sitting down to implement this (it isn’t out-of-the-box behavior) for the Metro XAML list controls (ListBox, ListView, GridView), I figured I had several options.  First, I could just handle the SelectionChanged event and in the codebehind I could programmatically bring up and/or collapse the page’s app bar(s).  That would do for one-off code, but its hardly the approach I would want to take for a more robust application.  A second option is to bind the list’s SelectedItem(s) property to property on the page’s ViewModel, and either use a related property or a ValueConverter to bind to the AppBar’s properties.  This felt a little bit much for wanting to simply alter the behavior of one control to react to the behavior another control.  There are other solutions that fell into this category as well (ViewState etc.)  What I ended up coming up with is a quasi-Behavior using Attached Properties that makes this (ahem) behavior reusable and quite easy to wire up.

Note: Another similar approach would be to actually use Behavior<T>.  Although this component of the Blend SDK is not included with the WinRT tools, some folks have published the equivalent WinRtBehaviors project on CodePlex at http://winrtbehaviors.codeplex.com/.

As I mentioned, at the heart of this implementation are an attached property and a set of Flags indicating which app bar(s) should react to the selection change.

   1: [Flags]

   2: public enum AppBarDisplayFlags

   3: {

   4:     None = 0,

   5:     Bottom = 1,

   6:     Top = 2,

   7:     BottomAndTop = 3,

   8: }

   9:  

  10: public static readonly DependencyProperty AppBarDisplayOnListSelectionProperty = 

  11:     DependencyProperty.RegisterAttached(

  12:         "AppBarDisplayOnListSelection", 

  13:         typeof(AppBarDisplayFlags), 

  14:         typeof(Selector), 

  15:         new PropertyMetadata(AppBarDisplayFlags.None, OnAppBarDisplayOnListSelectionChanged));

  16:  

  17: public static void SetAppBarDisplayOnListSelection(Selector element, AppBarDisplayFlags value)

  18: {

  19:     element.SetValue(AppBarDisplayOnListSelectionProperty, value);

  20: }

  21:  

  22: public static AppBarDisplayFlags GetAppBarDisplayOnListSelection(Selector element)

  23: {

  24:     return (AppBarDisplayFlags)element.GetValue(AppBarDisplayOnListSelectionProperty);

  25: }

Nothing really fancy there…just a simple attached property – I opted to indicate the owner as the Selector class instead of the containing class simply for convenience.  The important part is that the attached property is defined with a callback to be used when the value of the attached property is changed – OnAppBarDisplayOnListSelectionChanged.

In the property changed handler, a check is performed to see if the code is running in the designer – if so, everything bails out.  Otherwise, the selector to whom the property is being applied is obtained (if not found, bail out).  The selector and the value of the flags are then passed to a helper method to handle hooking up to the pertinent events.

   1: private static void OnAppBarDisplayOnListSelectionChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)

   2: {

   3:     // Don't hook up the event listeners when running in the designer

   4:     if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;

   5:  

   6:     // Identify the selector to which this property has been applied (if none, then do nothing)

   7:     var selector = dependencyObject as Selector;

   8:     if (selector == null) return;

   9:  

  10:     var selectedFlags = (AppBarDisplayFlags)dependencyPropertyChangedEventArgs.NewValue;

  11:     HookEvents(selector, selectedFlags);

  12: }

In HookEvents, as long as one of the app bars is of interest, listeners are registered for the selector’s Unloaded and SelectionChanged events.  Some preemptive housekeeping is also performed to first unhook the same events in order to prevent leaks.  The Unloaded event merely unhooks these same events to once again prevent leaks.

The big workhorse is the HandleSelectionChanged event handler.  First, the Visual Tree is traversed until an ancestor of the Selector is found that happens to be a Page – which is presumed to be the site of the app bar(s) being affected.  Then the current state of the setting to show the top and/or bottom app bars is determined, and finally, if an item is selected, the appropriate app bars are shown.  If no item is selected, the appropriate app bars are collapsed.

   1: private static void HandleSelectionChanged(Object sender, SelectionChangedEventArgs e)

   2: {

   3:     var selector = sender as Selector;

   4:     if (selector == null) return;

   5:  

   6:     // Traverse the selector's parents to find the firet "page" element

   7:     var containingPage = selector.GetVisualAncestors().OfType<Page>().FirstOrDefault();

   8:     if (containingPage == null) return;

   9:  

  10:     var currentFlags = GetAppBarDisplayOnListSelection(selector);

  11:     var showBottomAppBar = (currentFlags & AppBarDisplayFlags.Bottom) == AppBarDisplayFlags.Bottom;

  12:     var showTopAppBar = (currentFlags & AppBarDisplayFlags.Top) == AppBarDisplayFlags.Top;

  13:  

  14:     if (selector.SelectedItem != null)

  15:     {

  16:         // An item has been selected - show the relevant app bars

  17:         if (showBottomAppBar) ShowAppBar(containingPage.BottomAppBar);

  18:         if (showTopAppBar) ShowAppBar(containingPage.TopAppBar);

  19:     }

  20:     else

  21:     {

  22:         // Nothing has been selected - hide the relevant app bars

  23:         if (showBottomAppBar) HideAppBar(containingPage.BottomAppBar);

  24:         if (showTopAppBar) HideAppBar(containingPage.TopAppBar);

  25:     }

  26: }

Once the project containing this code has been compiled, the attached property is available to be set to Selector-derived UI elements.

   1: <GridView

   2:     x:Name="itemGridView"

   3:     AutomationProperties.AutomationId="ItemGridView"

   4:     AutomationProperties.Name="Grouped Items"

   5:     Margin="116,0,40,46"

   6:     ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"

   7:     ItemTemplate="{StaticResource Standard250x250ItemTemplate}"

   8:     SelectionMode="Multiple"

   9:     local:AppBarExtensions.AppBarDisplayOnListSelection="Bottom">

Obviously, this seems like a long way to travel – the value is realized when there are multiple Selector’s scattered throughout an application where this behavior is to be applied, and/or when this code is shared across multiple applications.  The entire code for the class containing the attached property follows:

   1: public static class AppBarExtensions

   2: {

   3:     [Flags]

   4:     public enum AppBarDisplayFlags

   5:     {

   6:         None = 0,

   7:         Bottom = 1,

   8:         Top = 2,

   9:         BottomAndTop = 3,

  10:     }

  11:  

  12:     public static readonly DependencyProperty AppBarDisplayOnListSelectionProperty = 

  13:         DependencyProperty.RegisterAttached(

  14:             "AppBarDisplayOnListSelection", 

  15:             typeof(AppBarDisplayFlags), 

  16:             typeof(Selector), 

  17:             new PropertyMetadata(AppBarDisplayFlags.None, OnAppBarDisplayOnListSelectionChanged));

  18:  

  19:     public static void SetAppBarDisplayOnListSelection(Selector element, AppBarDisplayFlags value)

  20:     {

  21:         element.SetValue(AppBarDisplayOnListSelectionProperty, value);

  22:     }

  23:  

  24:     public static AppBarDisplayFlags GetAppBarDisplayOnListSelection(Selector element)

  25:     {

  26:         return (AppBarDisplayFlags)element.GetValue(AppBarDisplayOnListSelectionProperty);

  27:     }

  28:  

  29:     private static void OnAppBarDisplayOnListSelectionChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)

  30:     {

  31:         // Don't hook up the event listeners when running in the designer

  32:         if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;

  33:  

  34:         // Identify the selector to which this property has been applied (if none, then do nothing)

  35:         var selector = dependencyObject as Selector;

  36:         if (selector == null) return;

  37:  

  38:         var selectedFlags = (AppBarDisplayFlags)dependencyPropertyChangedEventArgs.NewValue;

  39:         HookEvents(selector, selectedFlags);

  40:     }

  41:  

  42:     private static void HookEvents(Selector selector, AppBarDisplayFlags flags)

  43:     {

  44:         if (selector == null) throw new ArgumentNullException("selector");

  45:  

  46:         // Clear any "active" event handlers

  47:         UnhookEvents(selector);

  48:  

  49:         if (flags != AppBarDisplayFlags.None)

  50:         {

  51:             selector.Unloaded += HandleUnloaded;

  52:             selector.SelectionChanged += HandleSelectionChanged;

  53:         }

  54:     }

  55:  

  56:     private static void UnhookEvents(Selector selector)

  57:     {

  58:         if (selector == null) throw new ArgumentNullException("selector");

  59:  

  60:         selector.Unloaded -= HandleUnloaded;

  61:         selector.SelectionChanged -= HandleSelectionChanged;

  62:     }

  63:  

  64:     private static void HandleUnloaded(Object sender, RoutedEventArgs e)

  65:     {

  66:         UnhookEvents((Selector)sender);

  67:     }

  68:  

  69:     private static void HandleSelectionChanged(Object sender, SelectionChangedEventArgs e)

  70:     {

  71:         var selector = sender as Selector;

  72:         if (selector == null) return;

  73:  

  74:         // Traverse the selector's parents to find the firet "page" element

  75:         var containingPage = selector.GetVisualAncestors().OfType<Page>().FirstOrDefault();

  76:         if (containingPage == null) return;

  77:  

  78:         var currentFlags = GetAppBarDisplayOnListSelection(selector);

  79:         var showBottomAppBar = (currentFlags & AppBarDisplayFlags.Bottom) == AppBarDisplayFlags.Bottom;

  80:         var showTopAppBar = (currentFlags & AppBarDisplayFlags.Top) == AppBarDisplayFlags.Top;

  81:  

  82:         if (selector.SelectedItem != null)

  83:         {

  84:             // An item has been selected - show the relevant app bars

  85:             if (showBottomAppBar) ShowAppBar(containingPage.BottomAppBar);

  86:             if (showTopAppBar) ShowAppBar(containingPage.TopAppBar);

  87:         }

  88:         else

  89:         {

  90:             // Nothing has been selected - hide the relevant app bars

  91:             if (showBottomAppBar) HideAppBar(containingPage.BottomAppBar);

  92:             if (showTopAppBar) HideAppBar(containingPage.TopAppBar);

  93:         }

  94:     }

  95:  

  96:     private static void ShowAppBar(AppBar appBar)

  97:     {

  98:         if (appBar == null) return;

  99:  

 100:         appBar.IsSticky = true;

 101:         appBar.IsOpen = true;

 102:     }

 103:  

 104:     private static void HideAppBar(AppBar appBar)

 105:     {

 106:         if (appBar == null) return;

 107:  

 108:         appBar.IsOpen = false;

 109:         appBar.IsSticky = false;

 110:     }

 111:  

 112:     /// <summary>

 113:     /// Gets the ancestors of the element, up to the root.

 114:     /// </summary>

 115:     /// <param name="node">The element to start from.</param>

 116:     /// <returns>An enumerator of the ancestors.</returns>

 117:     public static IEnumerable<FrameworkElement> GetVisualAncestors(this FrameworkElement node)

 118:     {

 119:         var parent = node.GetVisualParent();

 120:         while (parent != null)

 121:         {

 122:             yield return parent;

 123:             parent = parent.GetVisualParent();

 124:         }

 125:     }

 126:  

 127:     /// <summary>

 128:     /// Gets the visual parent of the element.

 129:     /// </summary>

 130:     /// <param name="node">The element to check.</param>

 131:     /// <returns>The visual parent.</returns>

 132:     public static FrameworkElement GetVisualParent(this FrameworkElement node)

 133:     {

 134:         return VisualTreeHelper.GetParent(node) as FrameworkElement;

 135:     }

 136: }

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s