Sunday, 30 January 2011

MVVM-friendly hideable menus in WPF

I’ve been working on adding keyboard accessibility features to an existing MVVM WPF application - during the course of which I decided that while a menu bar would be a great way to aid navigation, it wasn’t in keeping with the aesthetic style.  A number of Microsoft applications (e.g. IE8+, Windows Live Messenger) solve this by having a menu bar that hides until the Alt or F10 key is pressed, then hides again when keyboard focus is lost.

This functionality is available OOB in MFC with the SetMenuBarVisibility member of the CFrameWnd class (also described in this article).  Despite searching for some time I couldn’t find how to surface this easily in WPF but I did find an answer on Stack Overflow, however it’s a bit too intrusive for MVVM.  In the end I came up with a solution that uses attached properties, which means no code-behind or view-based code in the view model.

The property is called HideMainMenu and it’s attached to a Window.  If set to true it will hide the first menu marked as the main menu.  It does this by hooking up code via an attached property to the Window.KeyDown and Menu.IsKeyboardFocus events.  I also borrow a little code from this question on Stack Overflow to help navigate the visual tree (the FindVisualChildren<T> method):

public static class MainMenuHelper
{
public static readonly DependencyProperty HideMainMenuProperty =
DependencyProperty.RegisterAttached("HideMainMenu",
typeof(bool), typeof(Window),
new FrameworkPropertyMetadata(false, OnWindowLoaded));

public static void SetHideMainMenu(UIElement element, bool value)
{
element.SetValue(HideMainMenuProperty, value);
}

public static bool GetHideMainMenu(UIElement element)
{
return (bool)element.GetValue(HideMainMenuProperty);
}

private static void OnWindowLoaded(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
if (!(bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
{
if ((bool)e.NewValue)
{
var window = sender as Window;
Debug.Assert(window != null);
window.Loaded += hostingWindow_Loaded;
}
}
}

private static void hostingWindow_Loaded(object sender, RoutedEventArgs e)
{
var window = sender as Window;

var menus = FindVisualChildren<Menu>(window);

foreach (var menu in menus)
{
if (menu.IsMainMenu)
{
window.KeyDown += hostingWindow_KeyDown;
menu.LostKeyboardFocus += mainMenu_LostKeyboardFocus;
menu.Visibility = Visibility.Collapsed;
break;
}
}

window.Loaded -= hostingWindow_Loaded;
}

private static void hostingWindow_KeyDown(object sender, KeyEventArgs e)
{
if (e.SystemKey == Key.LeftAlt || e.SystemKey == Key.RightAlt || e.SystemKey == Key.F10)
{
var window = sender as Window;
Debug.Assert(window != null);

foreach (Menu menu in FindVisualChildren<Menu>(window))
{
if (menu.IsMainMenu)
{
if (menu.Visibility == Visibility.Collapsed)
{
menu.Visibility = Visibility.Visible;
}
else
{
menu.Visibility = Visibility.Collapsed;
}

break;
}
}
}
}

private static void mainMenu_LostKeyboardFocus(
object sender, KeyboardFocusChangedEventArgs e)
{
var menu = sender as Menu;

if (menu.IsMainMenu && !menu.IsKeyboardFocusWithin)
{
menu.Visibility = Visibility.Collapsed;
}
}

private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
// thanks to http://stackoverflow.com/questions/974598/find-all-controls-in-wpf-window-by-type for this method

if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}

foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
}


Usage example as follows:



<Window x:Class="MenuSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ap="clr-namespace:MenuSample"
ap:MainMenuHelper.HideMainMenu="True"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Menu IsMainMenu="True">
<!-- menu items here -->
</Menu>
</Grid>
</Window>


The VS2010 sample project can be downloaded here.