Search Driven UI

Introduction

There are so many applications which have tonnes of functionality exposed through lot of menus, tool bars to click on. For applications like the discover-ability of features becomes a big issue since there are features buried deep down in the UI that most of the users never find. We typically see search paradigm being used for content but not for the functionality. Looking at Vista start menu, I thought exposing search for those kind of UI would be a great idea. In Vista, start menu has a search button that just searches over start menu and shows user the relevant programs that matches the search string. I thought, I would build a similar control for menu which can be used by application that have complex menu structure.

This way, Primary way user can find functionality is through search. If you think about search, I could see there are two main types of users…

  • Users who know what they are looking for but they just want to save time that they would spend navigating through complex menu. In this case they type in “New” for New Menu because they know ‘New’ exists
  • User who don’t know what they are looking for. This is a scenario which can be addressed through advanced search algorithms which not only looks for string matches but interprets what user is asking for. In this case they might type “Create” and New should show up as result.

Search I have implemented is rudimentary string.startswith() but one could imagine better/intelligent ways of doing search. This way not only application can narrow down UI that User needs to deal with, it can also provide way to just access the functionality without having to navigate through different menus.

This control provides following functionality…

  • Developer can wrap the existing menu control in the search menu control
  • provides a text box so that user can type in what they are looking for

pic1.JPG

  • It hides the menus that don’t meet user criteria

pic2.JPG

  • It also gives user option to just invoke the UI if there is only one match

pic5.JPG

  • It shows user multiple options if there are more than one matches where user can pick the option they think is right and invoke the functionality

pic3.JPG

pic4.JPG

Application Code

I am putting application code here first before I talk about how it is implemented because I wanted to first specify how I was expecting application developers to use this control. I did not want to dictate application developers to build their menu in certain ways. It could be lot cleaner to do this through commanding but that would dicate all app functionality to exposed through commanding. I will try to build a sample that does this through commanding (see in future work planned).

Here is a window.xaml from the test application that i was using while building this functionality. Here basically application author has built the menu, the way it would have been built typically. Then this menu is just wrapped inside the SearchMenuElement

<Window x:Class=”TestApp.Window1″
    xmlns=”
http://schemas.microsoft.com/winfx/2006/xaml/presentation
    xmlns:x=”
http://schemas.microsoft.com/winfx/2006/xaml
    xmlns:cp=”clr-namespace:Search;assembly=Search”
    xmlns:test=”clr-namespace:TestApp”
    Title=”TestApp”
    >
    <DockPanel>
       
<cp:SearchMenuElement>
            <cp:SearchMenuElement.SearchMenu>
            <Menu>
                <MenuItem Header=”File”>
                    <MenuItem Header=”New” Name=”New” Click=”OnMenuClick”/>
                    <MenuItem Header=”Open” Name=”Open” Click=”OnMenuClick”/>
                    <MenuItem Header=”Close” Name=”Close” Click=”OnMenuClick”/>
                    <MenuItem Header=”Save” Name=”Save” Click=”OnMenuClick”/>
                    <MenuItem Header=”Save As” Name=”SaveAs” Click=”OnMenuClick”/>
                    <MenuItem Header=”Exit” Name=”Exit” Click=”OnMenuClick”/>
                </MenuItem>
                <MenuItem Header=”Edit”>
                    <MenuItem Header=”ClipBoard”>
                        <MenuItem Header=”Cut” Name=”Cut” Click=”OnMenuClick”/>
                        <MenuItem Header=”Copy” Name=”Copy” Click=”OnMenuClick”/>
                        <MenuItem Header=”Paste” Name=”Paste” Click=”OnMenuClick”/>
                    </MenuItem>
                    <MenuItem Header=”Select All” Name=”SelectAll” Click=”OnMenuClick”/>
                </MenuItem>
                <MenuItem Header=”Tools”>
                    <MenuItem Header=”Options” Name=”Options” Click=”OnMenuClick”/>
                    <MenuItem Header=”Word Wrap” Name=”WordWrap” Click=”OnMenuClick” />
                    <MenuItem Header=”Font” Name=”Font” Click=”OnMenuClick”/>
                </MenuItem>
                <MenuItem Header=”Help”>
                    <MenuItem Header=”About” Name=”About” Click=”OnMenuClick”/>
                    <MenuItem Header=”Help Content”>
                        <MenuItem Header=”Search” Name=”Search” Click=”OnMenuClick”/>
                        <MenuItem Header=”Index” Name=”Index” Click=”OnMenuClick”/>
                    </MenuItem>
                </MenuItem>
            </Menu>
           
</cp:SearchMenuElement.SearchMenu>
        </cp:SearchMenuElement>

    </DockPanel>
</Window>

SearchMenuElement

This is custom framework element that i build to expose the search functionality. Application Authors will use this control as shown above.

Here is the code that you can compile as a DLL (name Search if you are using above XAML) to be able to use this control.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Collections;
using System.Windows.Input;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Media;

namespace Search
{
   
    public class SearchMenuElement : FrameworkElement
    {
        //Stores the menu structured passed by application
        private Menu _searchMenu = null;
        //used to enter the query string
        private TextBox _txtSearch = null;
        //list box to show matches for query
        private ListBox _lstSearchList = null;
        //flattened out collection for menu for searching
        private ArrayList _menuIndex = null;
        //collection that has matching results
        private ArrayList _resultMenu = null;
        //stack control where all control are added
        private StackPanel _stckBase = null;

        public SearchMenuElement():base()
        {
           
            //create Stack Panel
            _stckBase = new StackPanel();

            //Create Text Box
            _txtSearch = new TextBox();
            //adds a text changed event handler
            _txtSearch.TextChanged += new TextChangedEventHandler(_txtSearch_TextChanged);
            //adds a event handler to handle enter key
            _txtSearch.KeyDown += new KeyEventHandler(_txtSearch_KeyDown);
            //adds text box to stack panel
            _stckBase.Children.Add(_txtSearch);

            //Create ListBox
            _lstSearchList = new ListBox();
            //adds a event halder to handle the enter key
            _lstSearchList.KeyDown += new KeyEventHandler(_lstSearchList_KeyDown);
            //initial visibility is hidden, this is changed to visible when user enters the search query
            _lstSearchList.Visibility = Visibility.Hidden;
            //adds listbox to stack panel
            _stckBase.Children.Add(_lstSearchList);

            //Initialize Private data
            _menuIndex = new ArrayList();
            _resultMenu = new ArrayList();
          
        }

        // Override the default Measure method of framework element
        //it basically calls the measure on child stackpanel and returns its desired size
        protected override Size MeasureOverride(Size availableSize)
        {
            _stckBase.Measure(availableSize);
            return _stckBase.DesiredSize;
        }

        //override the default Arrange method of framework element
        //It basically calls the arrange on child stackpanel and returns its actual size.
        protected override Size ArrangeOverride(Size finalSize)
        {
            _stckBase.Arrange(new Rect(finalSize));
            return new Size(_stckBase.ActualWidth, _stckBase.ActualHeight);
        }

        //at the end of initialization adds menu set by user in application to stackpanel
        //adds stack panel to visual tree.
        public override void EndInit()
        {
            if (_searchMenu != null)
            {
                _stckBase.Children.Add(_searchMenu);
                foreach (MenuItem i in _searchMenu.Items)
                {
                    BuildIndex(i);
                }
                this.AddVisualChild(_stckBase);
            }
            base.EndInit();

        }

        //returns only visual child, stackpanel when index 0 is passed in
        protected override Visual GetVisualChild(int index)
        {
            if (index == 0)
                return _stckBase;
            else
                throw new IndexOutOfRangeException();
        }

        //returns 1 as VisualChildrenCount as there is only one visual child.
        protected override int VisualChildrenCount
        {
            get
            {
                //Only has one Visual Child StackPanel
                return 1;
            }
        }

        //handles the key board input for list box
        void _lstSearchList_KeyDown(object sender, KeyEventArgs e)
        {
            //invokes the menu if the enter key is pressed
            if ((sender as ListBox).SelectedIndex >= 0 && e.Key == Key.Enter)
            {
                InvokeMenu(_resultMenu[(sender as ListBox).SelectedIndex] as MenuItem);
            }
        }
       
        //invokes the menu through UI automation
        private void InvokeMenu(MenuItem selectedmenu)
        {
            AutomationPeer apr = UIElementAutomationPeer.CreatePeerForElement(selectedmenu);
            ((apr as MenuItemAutomationPeer) as IInvokeProvider).Invoke();
        }

        //handles the keyboard input for search text box
        void _txtSearch_KeyDown(object sender, KeyEventArgs e)
        {
            //handles the enter key
            if (e.Key == Key.Enter)
            {
                //if no matching results are found
                if (_resultMenu.Count == 0)
                {
                    MessageBox.Show(“No Items Found”);
                }
                //if there is exactly one match, then invoke the menu
                else if (_resultMenu.Count == 1)
                {
                    //execute menu
                    InvokeMenu((_resultMenu[0] as MenuItem));

                }
                //if there are more than one matches, then change focus to list box
                else
                {
                    _lstSearchList.Focus();
                    _lstSearchList.SelectedIndex = 0;

                }
            }
        }
       
        //On keyboar input queries the menu structure
        void _txtSearch_TextChanged(object sender, TextChangedEventArgs e)
        {
            SearchIndex((sender as TextBox).Text);
            if (_resultMenu.Count > 0 && _resultMenu.Count != _menuIndex.Count)
            {
                this._lstSearchList.Visibility = Visibility.Visible;
                this._lstSearchList.Height = 50;
                foreach (MenuItem i in _resultMenu)
                {
                    ListBoxItem li = new ListBoxItem();
                    MenuItem childmenu = i;
                    string menupath = null;
                    while (childmenu != null)
                    {
                        menupath = childmenu.Header.ToString() + “\\” + menupath;
                        childmenu = childmenu.Parent as MenuItem;
                    }
                    li.Content = menupath;
                    this._lstSearchList.Items.Add(li);
                }
            }
            else
            {
                this._lstSearchList.Visibility = Visibility.Hidden;
                this._lstSearchList.Height = 0;
            }
        }
                      
        //flattens out menu in collection so that it can be searched over
        private void BuildIndex(MenuItem root)
        {
            _menuIndex.Add(root);
            foreach (MenuItem i in root.Items)
            {
                BuildIndex(i);
            }
        }

        //iterates over the index
        private void SearchIndex(string searchstring)
        {
            //reset result
            _resultMenu.Clear();
            _lstSearchList.Items.Clear();
            foreach (MenuItem i in _menuIndex)
            {
                i.Visibility = Visibility.Hidden;
            }

            foreach (MenuItem i in _menuIndex)
            {
                if (i.Header.ToString().StartsWith(searchstring, StringComparison.OrdinalIgnoreCase))
                {
                    _resultMenu.Add(i);
                    MakeVisibleParents(i);
                    MakeVisibleChildren(i);

                }
            }
        }

        //for given matched menu, enable all its children
        private void MakeVisibleChildren(MenuItem parentmenu)
        {
            parentmenu.Visibility = Visibility.Visible;
            foreach (MenuItem child in parentmenu.Items)
            {
                MakeVisibleChildren(child);
            }
        }

        //for given matched menu, enable all its parents
        private void MakeVisibleParents(MenuItem childmenu)
        {
            while (childmenu != null)
            {
                childmenu.Visibility = Visibility.Visible;
                childmenu = childmenu.Parent as MenuItem;
            }
        }

        //property SearchMenu so that it can be set through XAML
        public Menu SearchMenu
        {
            get { return _searchMenu; }
            set
            {
                _searchMenu = value;
            }
        }

    }
}

 

Future Work planned…

Ideally the units of functionality that application provides is seperated out from the different UIs that might expose it. This is known as commanding. So the best way to do this is to actually search over command collection that application might have and invoke commands instead of actual UI like i do there. But that is for some other day…

Advertisements

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

%d bloggers like this: