In most applications certain user interface areas are shared among various modules. Examples are all kinds of menus, tool bars, status bars, Outlook style sidebars and others. What is common for all of them is that they serve as containers for collections items (menu items, toolbar buttons, etc.) that user can activate to trigger some actions in the application. For example, the
File > Open menu item normally would invoke the action of opening a file. However same action could be also accessible using a toolbar button. So it is desirable that we could decouple the user interface elements from the actions that they trigger. CAB provides solution for this in the form of
UIElements and
Commands.
UIExtensionSites are the shared containers that can contain your
UIElements. Out of the box CAB provides support for
MenuStrips and
ToolBarStrips but you can easily add support for other elements by implementing your own
UIElementAdapter. After you add a menu or toolbar to your SmartPart you need to register it as an
extension site, by calling the RegisterSite method on the WorkItem.UIExtensionsSites collection and providing unique identifier for that site. Usually extension sites are registered in the RootWorkItem by the developer of shell the application. Later when module developer needs to add his own items to that site he only need to create these items as usual and than call the Add method of the specific UIExtensionSite. The corresponding UIElementAdapter is responsible for properly showing items for given UIElement type.
Next step is to attach
Commands to your UIElements. Commands are used to assign easily the same code to multiple UIElements. To declare command you need to create event handler that implements your action. Then you add the
CommandHandler attribute to the method declaration specifying command name. To assign Command with a UIElement you can call the
AddInvoker method on specific Commands collection passing reference to the UIElement and specifying name of its event that will trigger the action.
All pages in the .NET PetShop sample share a common NavigationBar that contains links to following pages: Sign In, Sign Out, Edit Account, View Shopping Cart and the Search TextBox. So we can consider them as UIElements that invoke Commands in different modules. For example, SignIn command is handled in the Account module, and ViewShoppingCart is handled in the Orders module. So it would be nice if each module could register the commands it provides and the Shell doesn't need to know anything besides providing an extension site for them.
Because out of the box CAB only supports UIElementAdapters for toolbars and menus for now I will implement the navigation bar in form of the main menu to see how this works. Later I can implement a custom UIElementAdapter that would closely match the web style.
First I need to add a MenuStrip to the the ShellForm and make it publicly accessible. Then I can register it as UIExtensionSite by adding following line in ShellApplication's AfterShellCreated method:
RootWorkItem.UIExtensionSites.RegisterSite("NavBar", Shell.menuStrip);Basically that is all that shell developer needs to do -- the rest belongs to module developers. So I quickly switch hats, and go to the account management module. Let's start by adding the SignIn button. In the OnRunStarted method of AccountWorkItem I add following lines:
ToolStripMenuItem signInItem = new ToolStripMenuItem("SIGN IN");
UIExtensionSites["NavBar"].Add(signInItem);As you can see, first I create a normal ToolStripMenuItem. But then, instead of adding it directly to a MenuStrip, I add it to the UIExtendsionSite defined earlier. Internally CAB finds the MenuStrip defined earlier in ShellForm and passes it together with the menu item to the correct UIElementAdapter with knows how to handle them both correctly.
Now, when I run the application I should see my first menu item and I can add items for other two commands (SignOut and EditAccount) the same way. However, clicking all these menu items does nothing so I need to attach handlers for appropriate events on these items. I could to this the usual way by directly attaching instance method to the Click event but in CAB we should use Commands to handle such things. Following line will register the Click event of the SignIn menu item as invoker for the SignInCommand:
Commands["SignInCommand"].AddInvoker(signInItem, "Click");
To handle this command I need to declare the event handler and mark it with CommandHandler attribute:
[CommandHandler("SignInCommand")]
public void SignInHandler(object sender, EventArgs e)
{
SignIn();
}What is important is that command handlers and UI elements that invoke them doesn't need to be defined in the same class or in other way directly accessible. For example, the command could be invoked in one WorkItem and handled in another WorkItem, SmartPart or Presenter. And I guess that there can be multiple handlers as well as many invokers attached to single command but I haven't tried this yet.
In many cases command's availability depends of the application's state. For example, toolbar's undo button would be disabled if user hasn't made any modifications. In my case the SignIn button should be visible only if no user has authenticated and SignOut and My Account buttons should be visible otherwise. Normally I would control this by setting the Visible property on MenuItems but again in CAB I can do this indirectly using Commands. This ensures that if there are multiple UIElements for the same Command then all of them would get updated accordingly. Commands define the Status property that can have one of three values: Enabled, Disabled and Unavailable. When value of this property changes, the adapters linked to particular command can apply these changes to the corresponding UIElements. So in my case when user signs in or signs out I execute following code:
if (userAuthenticated)
Commands["CommandSignIn"].Status = CommandStatus.Unavailable;
else
Commands["CommandSignIn"].Status = CommandStatus.Enabled;