33
Page 1 of 33 DevLynx Blog - CAB Echoes from the Cave Oct 9 2007 th CAB Test Application At my day job, we are beginning to look at the next generation of our product line. The current generation is written in Borland Delphi and is starting to show it’s age. Over the years it has become tightly coupled and is increasingly difficult to maintain. I am currently investigating which technologies to use for our rewrite. We have already decided to develop in C#. Anything and everything else is still to be determined. A couple of months ago, I discovered Microsoft Composite Application Block (CAB). CAB was written by Microsoft’s patterns & practices team as a framework for decoupling large applications (http://www.cabpedia.com ). To test the capabilities of CAB I have decided to write an Address Book as a test application. Furthermore, I am going to attempt to blog as a means of documenting my thoughts and progress. This will be my first foray into the blogshere. My next entry will discuss my goals followed by a brief set of informal requirements for the test application. dlx

DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 1 of 33

DevLynx Blog - CABEchoes from the Cave

Oct 9 2007th

CAB Test Application

At my day job, we are beginning to look at the next generation of our product line. The currentgeneration is written in Borland Delphi and is starting to show it’s age. Over the years it hasbecome tightly coupled and is increasingly difficult to maintain. I am currently investigatingwhich technologies to use for our rewrite. We have already decided to develop in C#. Anythingand everything else is still to be determined.

A couple of months ago, I discovered Microsoft Composite Application Block (CAB). CAB waswritten by Microsoft’s patterns & practices team as a framework for decoupling largeapplications (http://www.cabpedia.com).

To test the capabilities of CAB I have decided to write an Address Book as a test application.Furthermore, I am going to attempt to blog as a means of documenting my thoughts andprogress. This will be my first foray into the blogshere.

My next entry will discuss my goals followed by a brief set of informal requirements for the testapplication.

dlx

Page 2: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 2 of 33

Oct 10 , 2007th

CAB Test App Goals

Users expect WinForms applications to provide a rich user experience (Ux). One of the maingoals of the test app is to determine if the CAB can support this type of Ux. The simple natureof the test app does not have the functionality to necessitate all of the controls with which I wantto experiment; therefore, some of the Ux will be a bit contrived.

A second major goal will be to experiment with Microsoft Enterprise Library.

I will be using controls from Developer Express (Dx) which provide an excellent collection ofWinForms controls. In addition, I will use the CAB DevExpress Extension Kit which providespre-built support for Dx controls within the CAB.

Ux Goals:1. Switch between a menu/toolbar and a Ribbon control in the same application. This is more

of a curiosity for me. Can navigation be abstracted enough so that it does not matter whattype of controls are used?

2. Nothing on the shell form should be pre-populated. It should consist of a menu/toolbar or aRibbon control and a statusbar.

3. The user should be able to use super tooltips or normal tooltips. When a command isdisabled the super tooltip information should change to inform the user how the commandcan be enabled.

4. The navigation system should be able to include button/menu item, checkbox, dropdownbuttons (sometimes called split buttons), galleries and button groups for the Ribbon, as wellas other controls (e.g. font name combobox).

5. Dx XtraBars includes a docking system. I need to determine how well they can be used asWorkspaces. The dockable panels need to be created on the fly and properly placed.

6. Buttons can handle two commands simultaneously. Need to investigate how well this works.7. Navigation can also be done with the so called Outlook bar. Need to experiment with this

type of navigation.8. For long processes the user expects the application to show progress. Will need to add a

progress bar to the statusbar on the fly and then remove it when it is no longer needed.9. Since the CAB only adds items to the right and bottom of UIElements we need to have

some method to create them in a more specific order regardless of the module load order.10. Mapping: I want to embed a mapping tool into the test app (it is an address book after all).

See what it would take to use either Microsoft Live Search Maps or Google Maps.

Other Goals:1. Decoupled: this is, of course, redundant; the CAB is all about decoupling.2. Globalization: all strings, images and other Ux elements must come from a resource file.3. Security: CAB can restrict module loading via role based security. So some parts of the test

Page 3: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 3 of 33

application must be secured. Look into also using the Encryption Application Block toprovide additional functionality.

4. Logging: the test app should log significant events. Use the Logging Application Block toprovide this functionality.

5. Configuration: the test app should not access the registry. The app should provide for sharedas well as user configuration files. Use the Configuration Application Block to provide thisfunctionality.

6. Database agnostic: Use Dx eXpress Persistent Objects (XPO) an object-relational mapping(ORM) tool. I’m not sure that we will use an ORM for our big rewrite at my day job, but Iwant to experiment with it here.

dlx

Page 4: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 4 of 33

Oct 13, 2007

CAB Test App - Features

I will be designing and implementing an address book as a test app. I chose an address book for avery pragmatic reason. My sister was visiting a couple of weeks ago and was searching for asimple address book app to organize her personal contacts; I was searching for a small project totest the CAB. It didn’t take a bolt of lightning for us to realize the mutual benefits.

Required Components:1. Name will have various components including: Display Name, First Name, Last Name,

Title, Suffix, Nickname2. Address will include the usual items: Street (multiline), City, State/Province, Postal Code,

Countrya. Multiple names can be associated with a single address.b. Multiple addresses can be associated with a single person - (home, work, summer house,

etc.) This is not actually a required feature but it will give me a chance to delve into themore sophisticated aspects of Dx eXpress Persistent Objects (XPO). This feature will testadding the address type into the many-many relationship linking object in XPO.

3. Phone number will include the number and what type of phone (home, cell, work, fax, etc.)A phone number can be associated with a name or an address.

4. Email address and type (personal, work, etc.)5. Website and type (personal, work, etc.)6. Notes associated with the name or address7. Tags - tags provide a way to filter names. The user will be able to create tags such as Friends,

Family, Business, Xmas Card, etc. Any number of tags can be associated with aname/address pair.

8. Reporting and Labels - need to support a variety of labels for printing as well as datareporting.a. I will be using Dx XtraReports for reporting.b. All reports will be stored externally and loaded automatically by the app. This allows me

to create a new report and give it to users (OK, my sister) without recompiling the app.c. Users must be able to create custom reports which are indistinguishable from reports that

are shipped with the app. This allows users to share their reports.9. Mapping - Addresses must be able to be mapped using Google Maps or Microsoft Live

Search Maps.

Nice-to-have Features:1. Important dates associated with names (Birthday, Anniversary, etc.)2. Other information associated with names (Gender, Picture, Avitar, etc.)3. Name styles - depending on the occasion, we want to print “Dr. John Smith III” on an

address label and other times we want to print “Johnny Smith”. Supplying a method to enterthis information and select the name style at print time would be useful.

Page 5: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 5 of 33

4. Use the USPS web services to create standardized addresses and get Zip Code information5. vCard support6. Be able to backup the data7. Tickler dates and times - this would allow me to play with a web service that stores the

information and sends email at the appropriate time.8. Share and merge data from multiple address books via a web service. With this feature, I can

let my sister maintain family and mutual friends addresses and I reap the benefits (bwah-ha-ha!!).

Since this is a test app (and a fairly simple one) this list will be the sum total of my requirementsdocument.

No doubt other features will creep into the app to fulfill all of the goals that I have for this test.If a new feature is significant I will address it in a future blog.

dlx

Page 6: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 6 of 33

Oct 23, 2007

Starting the CAB Test App

I started this blog to document what I am doing while creating a CAB project. Thisdocumentation accomplishes a number of things: recording my thoughts and implementationenables me to solidify the process in my own mind. It also provides bread crumbs for creating thelarger project at the day job. And finally, it allows others who are experimenting with the CABto gain some benefit from my thought process.

These entries will become a “how to” on CAB. There will be a lot of code snippets anddiscussions of implementation. So unless you have “embraced your inner geek” (or you are one ofmy family members and obligated to read this) now is the time to bail out ;-)

At the day job we have four or five applications which could take advantage of the CAB. So myfirst (erroneous) thought was that there would be four or five shell applications generated by theSCSF - one for each product. The same basic code would be present in all of these shells, adirect violation of “Don’t Repeat Yourself” (DRY). What really needs to be done is to create aproduct independent shell; all applications will use this common shell.

NOTE: This is a test app - I am not (yet) a CAB expert and have not written any CAB appsbeyond creating a “Hello World” example. If I had enough experience with the CAB...well, Iwouldn’t be writing this test app. So what you see here will almost certainly change throughoutthe development process (hey, just like a normal project). I’ll add and backtrack as I learn newthings or program myself into a corner. So, if you see me doing something wrong (or just plainstupid), or you see a better way, please let me know. After the next entry I’ll start posting fullsource code so that you can see the entire picture.

NOTE 2: I am assuming that you know the basics of both Visual Studio, Developer Expresscontrols, and have worked through at least the SCSF Hands On Labs.

Our first task is to create the product independent shell application. We will make codemodifications to use the Developer Express controls and allow the user to select either a standardmenu or the Office 2007 Ribbon control.

Off to work: create a new Smart Client Solution, uncheck “Create a separate module to definethe layout for the shell”. Since this is product independent we will take a minimalist approach forthe shell; another, product specific, module will define the layout.

In the Infrastructure.Library project, add a reference to CABDevExpress. In theSmartClientApplication.cs change the FormShellApplication to an XtraFormApplication.

In ShellForm.cs change the form to descend from the Dx XtraForm. In the ShellForm (design

Page 7: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 7 of 33

mode) ‘cry havoc’ and delete all controls. Add a Dx BarManager, DefaultLookAndFeel and docka DeckWorkspace to fill the rest of the form. In the Dx toolbar delete the default toolbar. Sincewe want this to work for all applications the form should define only the minimal layout.Toolbars, commands and status bar items will all be created by application specific modules.

Add the appropriate references and using statements. Rename components to something moredescriptive...in other words, cleanup work. In ShellForm.cs we will need to remove thereferences to the status bar label and the exit menu item.

After cleanup, my resulting ShellForm.cs is:namespace DevLynx.ShellApp.Infrastructure.Shell{ using DevLynx.ShellApp.Infrastructure.Interface.Constants; using DevExpress.XtraEditors; using DevExpress.XtraBars;

/// <summary> /// Main application shell view. /// </summary> public partial class ShellForm : XtraForm, ISmartPartInfoProvider { private Microsoft.Practices.CompositeUI.SmartParts. SmartPartInfoProvider infoProvider;

/// <summary> /// Default class initializer. /// </summary> public ShellForm() { InitializeComponent(); this.infoProvider = new Microsoft.Practices.CompositeUI. SmartParts.SmartPartInfoProvider(); layoutWorkspace.Name = WorkspaceNames.LayoutWorkspace; }

internal Bar MainMenu { get { return mainMenu; } }

internal Bar MainStatus { get { return statusBar; } }

internal Bars MainToolbar { get { return barManager.Bars; } }

public Microsoft.Practices.CompositeUI.SmartParts.ISmartPartInfo GetSmartPartInfo(Type smartPartInfoType) { Microsoft.Practices.CompositeUI.SmartParts. ISmartPartInfoProvider ensureProvider = this; return this.infoProvider.GetSmartPartInfo(smartPartInfoType); } }}

Page 8: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 8 of 33

Next we need to make modifications to show either a Ribbon form or a menu/toolbar form. Ourfirst step in this feature is to create a Dx Ribbon form. In the design mode of the Ribbon form,remove the default ribbon page - we want as clean of a form as possible. Add aDefaultLookAndFeel, an ApplicationMenu and dock a DeckWorkspace to fill the rest of theform. Do the same type of cleanup as in the menu based form.

We also want to expose the Ribbon, status bar and application menu so that UI items can beadded.

My resulting ShellRibbonForm is:namespace DevLynx.ShellApp.Infrastructure.Shell{ using System; using DevExpress.XtraBars; using Microsoft.Practices.CompositeUI.SmartParts; using DevExpress.XtraBars.Ribbon; using Microsoft.Practices.CompositeUI.EventBroker; using DevLynx.ShellApp.Infrastructure.Interface; using DevLynx.ShellApp.Infrastructure.Interface.Constants;

public partial class ShellRibbonForm : DevExpress.XtraBars.Ribbon.RibbonForm, ISmartPartInfoProvider { private Microsoft.Practices.CompositeUI.SmartParts. SmartPartInfoProvider infoProvider;

public ShellRibbonForm() { InitializeComponent(); this.infoProvider = new Microsoft.Practices.CompositeUI. SmartParts.SmartPartInfoProvider(); layoutWorkspace.Name = WorkspaceNames.LayoutWorkspace; }

internal RibbonControl MainMenu { get { return ribbon; } }

internal RibbonStatusBar MainStatus { get { return ribbonStatusBar; } }

internal ApplicationMenu ApplicationMenu { get { return applicationMenu; } }

public Microsoft.Practices.CompositeUI.SmartParts.ISmartPartInfo GetSmartPartInfo(Type smartPartInfoType) { Microsoft.Practices.CompositeUI.SmartParts. ISmartPartInfoProvider ensureProvider = this; return this.infoProvider.GetSmartPartInfo(smartPartInfoType); } }}

Page 9: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 9 of 33

We have now created the UI elements but we cannot yet choose between the two forms. Herewe turn to the ShellApplication class. ShellApplication currently contains the Main methodentry point which instantiates the ShellForm via the ShellApplication class. So we want toextract a class which just contains the Main method entry point. The new ShellMain class willcall either the ShellApplication (standard menu) or it will call the ShellRibbonApplication(Ribbon control). The ShellMain class will reside in the Shell project.

My ShellMain.cs class:namespace DevLynx.ShellApp.Infrastructure.Shell{ using System; using System.Configuration; using System.Collections.Specialized; using System.Windows.Forms; using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling; using DevExpress.XtraEditors;

/// <summary> /// Main application entry point class. /// </summary> class ShellMain { /// <summary> /// Application entry point. /// </summary> [STAThread] static void Main() { // grab the setting for which form to show from AppSetting // in the configuration file. NameValueCollection appSettings = ConfigurationManager.AppSettings; string IsRibbonForm = appSettings.Get("IsRibbonForm"); bool isRibbon = Convert.ToBoolean(IsRibbonForm); bool enableFormSkins = false;

if (!isRibbon) // Don't use the bonus skins for Ribbons because I don't think // they follow the Microsoft Ribbon Guidelines closely enough. DevExpress.UserSkins.BonusSkins.Register();

DevExpress.UserSkins.OfficeSkins.Register(); DevExpress.Skins.SkinManager.Default. RegisterAssembly(typeof( DevExpress.UserSkins.Office2007Bonus).Assembly); if (enableFormSkins) DevExpress.Skins.SkinManager.EnableFormSkins();

#if (DEBUG) if (isRibbon) ShellRibbonApplication.RunInDebugMode(); else ShellApplication.RunInDebugMode();#else // (RELEASE) if (isRibbon) ShellRibbonApplication.RunInReleaseMode(); else ShellApplication.RunInReleaseMode();#endif // if (DEBUG)

Page 10: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 10 of 33

} }}

The modified ShellApplication class controls the standard menu form but without the Mainmethod entry point. My modified ShellApplication.cs class is:

namespace DevLynx.ShellApp.Infrastructure.Shell{ using System; using DevLynx.ShellApp.Infrastructure.Library; using Microsoft.Practices.CompositeUI; using DevLynx.ShellApp.Infrastructure.Interface.Constants; using CABDevExpress.UIElements; using System.Windows.Forms; using DevExpress.XtraEditors; using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;

class ShellApplication : SmartClientApplication<WorkItem, ShellForm> { /// <summary> /// Sets the extension site registration after the shell has been created. /// </summary> protected override void AfterShellCreated() { base.AfterShellCreated();

RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.MainMenu, this.Shell.MainMenu); RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.MainStatus, this.Shell.MainStatus); RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.MainToolbar, new BarsUIAdapter(this.Shell.MainToolbar)); }

internal static void RunInDebugMode() { Application.SetCompatibleTextRenderingDefault(false); new ShellApplication().Run(); }

internal static void RunInReleaseMode() { AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppDomainUnhandledException); Application.SetCompatibleTextRenderingDefault(false); try { new ShellApplication().Run(); } catch (Exception ex) { HandleException(ex); } }

private static void AppDomainUnhandledException( object sender, UnhandledExceptionEventArgs e) { HandleException(e.ExceptionObject as Exception); }

private static void HandleException(Exception ex)

Page 11: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 11 of 33

{ if (ex == null) return;

ExceptionPolicy.HandleException(ex, "Default Policy"); XtraMessageBox.Show("An unhandled exception occurred, and the application is terminating. For more information, see your Application event log."); Application.Exit(); } }}

We now need a corresponding class to handle the Ribbon control. Create a newShellRibbonApplication.cs to instantiate the ShellRibbonForm. My ShellRibbonApplication.cs:

namespace DevLynx.ShellApp.Infrastructure.Shell{ using System; using System.Windows.Forms; using Microsoft.Practices.CompositeUI; using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling; using DevLynx.ShellApp.Infrastructure.Library; using DevLynx.ShellApp.Infrastructure.Interface.Constants; using DevExpress.XtraEditors;

internal class ShellRibbonApplication : SmartClientApplication<WorkItem, ShellRibbonForm> { internal static void RunInDebugMode() { Application.SetCompatibleTextRenderingDefault(false); ShellRibbonApplication app = new ShellRibbonApplication(); app.Run(); }

internal static void RunInReleaseMode() { AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppDomainUnhandledException); Application.SetCompatibleTextRenderingDefault(false); try { ShellRibbonApplication app = new ShellRibbonApplication(); app.Run(); } catch (Exception ex) { HandleException(ex); } }

protected override void BeforeShellCreated() { base.BeforeShellCreated(); }

/// <summary> /// Sets the extension site registration after the shell has been created. /// </summary> protected override void AfterShellCreated() { base.AfterShellCreated();

Page 12: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 12 of 33

RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.MainMenu, this.Shell.MainMenu); RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.MainStatus, this.Shell.MainStatus); //Unfortunately we don't yet have a UIExtensionSite for an //ApplicationMenu - we will create one of these later //RootWorkItem.UIExtensionSites.RegisterSite( // UIExtensionSiteNames.ApplicationMenu, // this.Shell.ApplicationMenu); }

private static void AppDomainUnhandledException( object sender, UnhandledExceptionEventArgs e) { HandleException(e.ExceptionObject as Exception); }

private static void HandleException(Exception ex) { if (ex == null) return;

ExceptionPolicy.HandleException(ex, "Default Policy"); XtraMessageBox.Show("An unhandled exception occurred, and the application is terminating. For more information, see your Application event log."); Application.Exit(); } }}

Obviously when you compile you will need to resolve all of the little changes that were notdescribed here as well as adding references to the Dx specific modules. In the next entry Iwill supply source code so that you can see the full code in context.

We have created the foundation for a general purpose shell application which supports eithera standard menu or a Ribbon control. In the next entry we will implement functionality toadd buttons/menu items to either of the two forms.

dlx

Page 13: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 13 of 33

Oct 28, 2007Abstracting CAB Commands with Developer Express Controls

In order to support either the Ribbon control or the menu/toolbar paradigm we mustabstract the creation of UI extensions. When using menu items or buttons, either on theRibbon or toolbar, Developer Express makes this easy. Both menu items and buttons use aBarButtonItem. This allows us to abstract where the instance of BarButtonItem is placed.

To facilitate the abstraction I have created a UxExtension service that any CAB WorkItemcan call to add a BarButtonItem. The BarButtonItem creation is described by theUxExtensionEventArgs class. This class will eventually contain members that will describeall of the BarButtonItem properties so that a rich user experience can be obtained. However,the first iteration just provides enough information to get the button into the user interface.

The UxExtensionEventArgs:namespace DevLynx.ShellApp.Infrastructure.Interface{ using System; using System.Drawing;

/// <summary> /// Describes a button or menu item that will be added /// to the user interface. UxExtensionEventArgs abstracts /// the information enough so that we can diplay either a /// menu/toolbar or Ribbon paradigm to the user. This is /// possible because Developer Express uses a common object /// (<see cref="DevExpress.XtraBars.BarButtonItem"/>) for /// it's menus, toolbars and the Ribbon control. /// Thank you Deverloper Express! /// </summary> public class UxExtensionEventArgs : EventArgs { /// <summary> /// Initializes a new instance of the /// <see cref="UxExtensionEventArgs"/> class. /// </summary> public UxExtensionEventArgs() { CommandRank = CommandRank.Default; Invoker = "ItemClick"; }

private string commandName; /// <summary> /// Gets or sets the name of the CAB command. /// </summary> /// <value>The name of the CAB command.</value> public string CommandName { get { return commandName; } set { commandName = value; } }

Page 14: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 14 of 33

private string invoker; /// <summary> /// Gets or sets the command invoker which is the event /// name that is fired when the command is invoked. /// </summary> /// <value>The name of the invoker event.</value> public string Invoker { get { return invoker; } set { invoker = value; } }

private string text; /// <summary> /// Gets or sets the text displayed with the button. /// </summary> /// <value>The text.</value> public string Text { get { return text; } set { text = value;} }

private Image smallImage; /// <summary> /// Gets or sets the small image displayed on a /// smaller button. /// </summary> /// <value>The image.</value> public Image SmallImage { get { return smallImage; } set { smallImage = value; } }

private Image largeImage; /// <summary> /// Gets or sets the large image displayed on a /// larger button. /// </summary> /// <value>The image.</value> public Image LargeImage { get { return largeImage; } set { largeImage = value; } }

private CommandRank commandRank; /// <summary> /// Gets or sets the <see cref="CommandRank"/> which decides /// how the command is displayed on the ribbon or menu/toolbar. /// </summary> /// <value>The command rank.</value> public CommandRank CommandRank { get { return commandRank; } set { commandRank = value; } }

Page 15: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 15 of 33

private string page; /// <summary> /// Gets or sets the ribbon page where this command is to /// be placed. /// </summary> /// <value>The ribbon page.</value> public string Page { get { return page; } set { page = value; } }

private string group; /// <summary> /// Gets or sets the ribbon group where this command is to /// be placed. /// </summary> /// <value>The ribbon group.</value> public string Group { get { return group; } set { group = value; } }

private string menu; /// <summary> /// Gets or sets the menu where this command is to be /// placed. /// </summary> /// <value>The menu.</value> public string Menu { get { return menu; } set { menu = value; } }

private string toolbar; /// <summary> /// Gets or sets the toolbar where this command is /// to be placed. If Toolbar is blank the command /// is not placed on any toolbar. /// </summary> /// <value>The toolbar.</value> public string Toolbar { get { return toolbar; } set { toolbar = value; } } }}

The UxExtension service currently provides a single method to add a command. TheAddCommand method takes the WorkItem that is adding the command and aUxExtensionEventArgs as parameters. The AddCommand determines which paradigm touse and calls either the AddDxRibbonCommand or the AddDxMenuCommand. TheUxExtension service will eventually have methods to add a top level menu, a toolbar and

Page 16: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 16 of 33

Ribbon pages and groups. The current UxExtension class:namespace DevLynx.ShellApp.Infrastructure.Library.Services{ using System; using Microsoft.Practices.CompositeUI; using DevExpress.XtraBars; using DevExpress.XtraBars.Ribbon; using DevLynx.ShellApp.Infrastructure.Interface.Constants; using DevLynx.ShellApp.Infrastructure.Interface; using DevLynx.ShellApp.Infrastructure.Interface.Services;

/// <summary> /// Implementation of the IUxExtension service interface. /// </summary> [Service(typeof(IUxExtension))] public class UxExtension : IUxExtension { private WorkItem rootWorkItem; private bool? formHasRibbonControl;

/// <summary> /// Initializes a new instance of the <see cref="UxExtension"/> /// class. /// </summary> /// <param name="rootWorkItem">The root work item.</param> public UxExtension([ServiceDependency] WorkItem rootWorkItem) { this.rootWorkItem = rootWorkItem; if (formHasRibbonControl == null) { IGlobalInformation globalInformation = rootWorkItem.Services.Get<IGlobalInformation>(true); formHasRibbonControl = globalInformation.FormHasRibbonControl; } }

/// <summary> /// Adds a CAB command to the user interface. /// </summary> /// <param name="workItem">The work item that is adding this /// command.</param> /// <param name="eventArgs">The <see cref="UxExtensionEventArgs"/> /// instance containing the command data.</param> /// <returns> /// The <see cref="DevExpress.XtraBars.BarButtonItem"/> /// that was added. /// </returns> public BarButtonItem AddCommand(WorkItem workItem, UxExtensionEventArgs eventArgs) { if ((bool)formHasRibbonControl) { return AddDxRibbonCommand(workItem, eventArgs); } else { return AddDxMenuCommand(workItem, eventArgs);

Page 17: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 17 of 33

} }

private BarButtonItem AddDxRibbonCommand(WorkItem workItem, UxExtensionEventArgs eventArgs) { AddRibbonInfrastructureIfNecessary(eventArgs.Page, eventArgs.Group); BarButtonItem button = InitializeButton(eventArgs); button.RibbonStyle = GetRibbonStyle(eventArgs.CommandRank); workItem.UIExtensionSites[GetRibbonGroupName(eventArgs.Page, eventArgs.Group)].Add<BarButtonItem>(button); workItem.Commands[eventArgs.CommandName].AddInvoker(button, eventArgs.Invoker); return button; }

private BarButtonItem AddDxMenuCommand(WorkItem workItem, UxExtensionEventArgs eventArgs) { AddMenuInfrastructureIfNecessary(eventArgs.Menu, eventArgs.Toolbar); BarButtonItem button = InitializeButton(eventArgs); workItem.UIExtensionSites[GetMainMenuName(eventArgs.Menu)]. Add<BarButtonItem>(button); workItem.Commands[eventArgs.CommandName].AddInvoker( button, eventArgs.Invoker);

if (!String.IsNullOrEmpty(eventArgs.Toolbar)) { workItem.UIExtensionSites[GetToolbarName( eventArgs.Toolbar)].Add<BarButtonItem>(button); } return button; }

/// <summary> /// Adds the menu/toolbar infrastructure if necessary. /// Menus and toolbars should be created prior to adding /// commands since more settings can be initialized. This /// is a last resort which will ensure that the menu and /// toolbars are created with default parameters if they /// do not already exist. /// </summary> /// <param name="menu">The menu in which this command will /// be added.</param> /// <param name="toolbar">The toolbar on which this command /// will be displayed.</param> private void AddMenuInfrastructureIfNecessary(string menu, string toolbar) { string mainMenuName = GetMainMenuName(menu); if (!rootWorkItem.UIExtensionSites.Contains(mainMenuName)) { BarSubItem mainMenuItem = new BarSubItem(); mainMenuItem.Caption = menu; rootWorkItem.UIExtensionSites[ UIExtensionSiteNames.MainMenu].

Page 18: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 18 of 33

Add<BarSubItem>(mainMenuItem); rootWorkItem.UIExtensionSites.RegisterSite( mainMenuName, mainMenuItem); } string toolbarName = GetToolbarName(toolbar); if (!String.IsNullOrEmpty(toolbar) && !rootWorkItem.UIExtensionSites.Contains(toolbarName)) { Bar toolbarItem = new Bar(); toolbarItem.Text = menu; toolbarItem.DockStyle = BarDockStyle.Top; rootWorkItem.UIExtensionSites[ UIExtensionSiteNames.MainToolbar]. Add<Bar>(toolbarItem); rootWorkItem.UIExtensionSites.RegisterSite( toolbarName, toolbarItem); } }

/// <summary> /// Adds the ribbon infrastructure if necessary. Pages and groups /// should be created prior to adding commands since more settings /// can be initialized. This is a last resort which will ensure /// that the pages and groups are created with default parameters /// if they do not already exist. /// </summary> /// <param name="page">The page on which this command will /// be displayed.</param> /// <param name="group">The group in which this command will /// be displayed.</param> private void AddRibbonInfrastructureIfNecessary(string page, string group) { string pageName = GetRibbonPageName(page); if (!rootWorkItem.UIExtensionSites.Contains(pageName)) { RibbonPage ribbonPage = new RibbonPage(page); rootWorkItem.UIExtensionSites[ UIExtensionSiteNames.MainMenu]. Add<RibbonPage>(ribbonPage); rootWorkItem.UIExtensionSites.RegisterSite( pageName, ribbonPage); } string groupName = GetRibbonGroupName(page, group); if (!rootWorkItem.UIExtensionSites.Contains(groupName)) { RibbonPageGroup ribbonGroup = new RibbonPageGroup(group); ribbonGroup.ShowCaptionButton = false; rootWorkItem.UIExtensionSites[pageName]. Add<RibbonPageGroup>(ribbonGroup); rootWorkItem.UIExtensionSites.RegisterSite(groupName, ribbonGroup); } }

private string GetRibbonGroupName(string page, string group) { return "Ribbon://Page." + page + "/Group." + group;

Page 19: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 19 of 33

}

private string GetRibbonPageName(string page) { return "Ribbon://Page." + page; }

private string GetMainMenuName(object menu) { return "MainMenu://" + menu; }

private string GetToolbarName(string toolbar) { return "ToolBar://" + toolbar; }

private RibbonItemStyles GetRibbonStyle(CommandRank commandRank) { switch (commandRank) { case CommandRank.Default: case CommandRank.Minor: return RibbonItemStyles.SmallWithText; case CommandRank.Major: return RibbonItemStyles.Large; case CommandRank.SubCommand: return RibbonItemStyles.SmallWithoutText; default: return RibbonItemStyles.Default; } }

/// <summary> /// Creates a <see cref="DevExpress.XtraBars.BarButtonItem"/> /// and initializes the button properties that are common to /// both the ribbon and menu/toolbar. /// </summary> /// <param name="eventArgs">The /// <see cref="DevLynx.ShellApp.Infrastructure.Interface. /// UxExtensionEventArgs"/> instance containing the /// command data.</param> /// <returns>The <see cref="DevExpress.XtraBars.BarButtonItem"/> /// that was created and initialized.</returns> private BarButtonItem InitializeButton(UxExtensionEventArgs eventArgs) { BarButtonItem button = new BarButtonItem(); button.Caption = eventArgs.Text; button.Glyph = eventArgs.SmallImage; button.LargeGlyph = eventArgs.LargeImage; return button; } }}

To add a command a module would instantiate an IUxExtension, populate a

Page 20: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 20 of 33

UxExtensionEventArgs, and call the IUxExtension AddCommand method. An example: public class ModuleController : WorkItemController { public override void Run() { ExtendUx(); }

private void ExtendUx() { AddApplicationCloseCommand(); }

private void AddApplicationCloseCommand() { UxExtensionEventArgs eventArgs = new UxExtensionEventArgs(); eventArgs.Text = Resources.ApplicationClose; eventArgs.CommandName = Constants.CommandNames.ApplicationClose; eventArgs.SmallImage = Resources.stop_16; eventArgs.LargeImage = Resources.stop_32; eventArgs.CommandRank = CommandRank.Major; eventArgs.Page = Resources.RibbonHomePage; eventArgs.Group = Resources.RibbonExitGroup; eventArgs.Menu = Resources.MenuFile; IUxExtension uxExtension = WorkItem.Services. Get<IUxExtension>(true); uxExtension.AddCommand(WorkItem, eventArgs); }

[CommandHandler(Constants.CommandNames.ApplicationClose)] public void ApplicationClose(object sender, EventArgs e) { WorkItem.EventTopics[EventTopicNames.ApplicationClose].Fire( this, new EventArgs<string>(""), WorkItem, PublicationScope.Global); }

When the UxExtension.AddCommand method is called the service will determine whichtype of form is currently active and add the appropriate buttons to the user interface.

Another problem with a generic AppShell is how to handle application specific icons andcaptions. There are currently four things that we currently have to set for the AppShell: theapplication title; the document or project name that may be displayed on the caption bar; theapplication icon; and the image in the Ribbon’s Application Menu. I have added an interfaceto both the ShellForm and the ShellRibbonForm to ensure that all of these items are takencare of. The IdevLynxCabMainApplicationForm interface:namespace DevLynx.ShellApp.Infrastructure.Interface{ using System; using System.Drawing;

/// <summary>

Page 21: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 21 of 33

/// Defines the methods required to show the application /// specific information in the general purpose ShellApp. /// </summary> public interface IDevLynxCabMainApplicationForm { /// <summary> /// Sets the application caption. Should be decorated with: /// <c>[EventSubscription(EventTopicNames. /// ApplicationCaptionUpdate, ThreadOption.UserInterface)]</c> /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="DevLynx.ShellApp. /// Infrastructure.Interface.EventArgs&lt;T&gt;"/> instance /// containing the event data.</param> void SetApplicationCaption(Object sender, EventArgs<string> e);

/// <summary> /// Sets the application document caption. Should be decorated /// with: <c>[EventSubscription(EventTopicNames. /// ApplicationDocumentCaptionUpdate, /// ThreadOption.UserInterface)]</c> /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="DevLynx.ShellApp.Infrastructure. /// Interface.EventArgs&lt;T&gt;"/> instance containing the event /// data.</param> void SetApplicationDocumentCaption(Object sender, EventArgs<string> e);

/// <summary> /// Sets the application ribbon icon. Should be decorated with: /// <c>[EventSubscription(EventTopicNames. /// ApplicationRibbonIconUpdate, ThreadOption.UserInterface)]</c> /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="DevLynx.ShellApp.Infrastructure. /// Interface.EventArgs&lt;T&gt;"/> instance containing the event /// data.</param> void SetApplicationRibbonIcon(Object sender, EventArgs<Bitmap> e);

/// <summary> /// Sets the application icon. Should be decorated with: /// <c>[EventSubscription(EventTopicNames.ApplicationIconUpdate, /// ThreadOption.UserInterface)]</c> /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="DevLynx.ShellApp.Infrastructure. /// Interface.EventArgs&lt;T&gt;"/> instance containing the event /// data.</param> void SetApplicationIcon(Object sender, EventArgs<Icon> e);

/// <summary> /// Closes the application. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="DevLynx.ShellApp.Infrastructure. /// Interface.EventArgs&lt;T&gt;"/> instance containing the event /// data.</param>

Page 22: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 22 of 33

void CloseApplication(Object sender, EventArgs<string> e); }}

Both the ShellForm and the ShellRibbonForm implement this interface. From a differentmodule these would be set using: protected virtual void SetApplicationInfo() { WorkItem.EventTopics[EventTopicNames.ApplicationCaptionUpdate]. Fire(this, new EventArgs<string>(Resources. ApplicationCaption), WorkItem, PublicationScope.Global); WorkItem.EventTopics[EventTopicNames. ApplicationDocumentCaptionUpdate].Fire(this, new EventArgs<string>( Resources.ApplicationDocumentCaption), WorkItem, PublicationScope.Global); WorkItem.EventTopics[EventTopicNames.ApplicationIconUpdate]. Fire(this, new EventArgs<Icon>(Resources.Gear_Blue), WorkItem, PublicationScope.Global); WorkItem.EventTopics[EventTopicNames. ApplicationRibbonIconUpdate]. Fire(this, new EventArgs<Bitmap>(Resources.Collie), WorkItem, PublicationScope.Global); }

We now have an application which allows either a Ribbon or a menu/toolbar user interface.We have abstracted the creation of the UI extensions to ease this choice; and we have createdCAB events to handle the application specific information such as the text in the applicationcaption bar.

You will find the complete source code for this example at:s3.amazonaws.com/DevLynx/DevLynx CAB 20071028.zip

dlx

Page 23: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 23 of 33

Nov 4, 2007More Generic Commands

In my last post I stated that Developer Express uses a common object for it's menus, toolbarsand the Ribbon control, the BarButtonItem. This is not quite correct. The actual ancestor isthe BarItem. The BarButtonItem covers the standard case (a button) but not all of thepossibilities. We have to make a few changes to get more generic commands. We haveabstracted the command structure with the UxExtensionEventArgs. The next step is toabstract which type of BarItem is created and inserted into the UI for the command.

Our goal is to replace references of BarButtonItem with BarItem and set up theinfrastructure to allow various BarItem descendants to be instantiated. Not all of the BarItemdescendants will work in the first iteration. We will make sure each descendent works as wedevelop them.

The first step is to change all references to BarButtonItem to BarItem with one exception: inthe InitializeButton method of UxExtension leave the create alone for now. Shell app shouldstill compile. I also changed all references to “button” to command or item.

The second step is to add a way to determine which descendant of BarItem to create. Wewill add a new property to the UxExtensionEventArgs class to store this information. InDelphi I would use:

TBarItems = class of BarItem;

For those not familiar with the Delphi construct “class of” refers to type metadata. We cannow defer the decision of which BarItem descendent to a later time. This allows Delphidevelopers to easily implement an abstract factory pattern. Some (non-useful) Delphi codeto do this is:

varItemType : TBarItems;Item : BarItem;

beginItemType := BarButtonItem;Item := ItemType.Create(); // we now have a BarButtonItem.

end;

In C# we use the generic Type class to store the item type and do a bit of runtime typechecking: private Type itemType; /// <summary> /// Gets or sets the type of the bar item for this command. /// </summary> /// <value>The type of the bar item for this command.</value> public Type ItemType { get { return itemType; } set { Guard.TypeIsAssignableFromType(value, typeof(BarItem),

Page 24: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 24 of 33

"BarItem"); itemType = value; } }

Here I use the “Guard” class to ensure that the type passed in is acceptable during runtime.The Guard class is part of the CAB. Any class that is not assignable to a BarItem will causean exception. Acalling method will be required to use typeof(BarButtonItem) to set theItemType. There must be an elegant way to use Generics here but I’m not exactly sure howto accomplish that. If and when I figure it out I’ll make modifications.

Now we have to instantiate this BarItem descendant within the UxExtension class. In theInitializeCommand method replace: BarButtonItem button = new BarButtonItem(); with:

BarItem barItem = (BarItem)Activator.CreateInstance(eventArgs.ItemType, true);

Using the Activator.CreateInstance allows us to properly defer the decision of whichBarItem descendant to employ until we are ready to create it.

Finally, we add a default to the UxExtensionEventArgs. I imagine the most oft used classwill be the BarButtonItem. So we add the following line to the UxExtensionEventArgsconstructor:

ItemType = typeof(BarButtonItem);

We can now use any of the BarItem descendants in our commands. Our next task is to createUIElementAdaptor’s for the Ribbon ApplicationMenu and the RibbonPageHeader. TheRibbonPageHeader is new with the Developer Express 7.3 release and allows adding ofBarItems on the empty right side of the Ribbon Page tabs.

dlx

Page 25: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 25 of 33

Nov 6, 2007ApplicationMenu and RibbonPageHeader UIElementAdaptors

This weekend I did a little work on adding a UIElementAdaptor for the RibbonApplicationMenu. I also worked on a UIElementAdaptor for the RibbonPageHeader whichis new to the Developer Express v2007 vol 3 beta. The RibbonPageHeader allows theaddition of buttons and other BarItems to the right side of the Ribbon tabs area.

The code for the ApplicationMenuUIAdaptor:using System;using DevExpress.XtraBars;using Microsoft.Practices.CompositeUI.UIElements;using Microsoft.Practices.CompositeUI.Utility;using DevExpress.XtraBars.Ribbon;

namespace CABDevExpress.UIElements{ /// <summary> /// An adapter that wraps an <see cref="ApplicationMenu"/>

/// for use as an <see cref="IUIElementAdapter"/>. /// </summary> public class ApplicationMenuUIAdapter : UIElementAdapter<BarItem> { private ApplicationMenu applicationMenu;

/// <summary> /// Initializes a new instance of the /// <see cref="ApplicationMenuUIAdapter"/> class. /// </summary> /// <param name="applicationMenu"></param> public ApplicationMenuUIAdapter(ApplicationMenu applicationMenu) { Guard.ArgumentNotNull(applicationMenu, "applicationMenu"); this.applicationMenu = applicationMenu; }

/// <summary> /// See <see cref="UIElementAdapter{TUIElement}.Add(TUIElement)"/> /// for more information. /// </summary> protected override BarItem Add(BarItem uiElement) { Guard.ArgumentNotNull(uiElement, "uiElement"); if (applicationMenu == null) throw new InvalidOperationException();

applicationMenu.AddItem(uiElement); return uiElement; }

/// <summary> /// See <see cref="UIElementAdapter{TUIElement}. /// Remove(TUIElement)"/> for more information. /// </summary> protected override void Remove(BarItem uiElement)

Page 26: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 26 of 33

{ Guard.ArgumentNotNull(uiElement, "uiElement"); if (applicationMenu == null) throw new InvalidOperationException(); if (applicationMenu.Ribbon == null) throw new InvalidOperationException();

applicationMenu.Ribbon.Items.Remove(uiElement); } }}

The code for the RibbonPageHeaderUIAdaptor:using System;using DevExpress.XtraBars.Ribbon;using DevExpress.XtraBars;using Microsoft.Practices.CompositeUI.UIElements;using Microsoft.Practices.CompositeUI.Utility;

namespace CABDevExpress.UIElements{ /// <summary> /// An adapter that wraps a /// <see cref="RibbonPageHeaderItemLinkCollection"/> /// for use as an <see cref="IUIElementAdapter"/>. /// </summary> public class RibbonPageHeaderUIAdapter : UIElementAdapter<BarItem> { private RibbonPageHeaderItemLinkCollection ribbonPageHeader;

/// <summary> /// Initializes a new instance of the /// <see cref="RibbonPageHeaderUIAdapter"/> class. /// </summary> /// <param name="ribbonPageHeader"></param> public RibbonPageHeaderUIAdapter( RibbonPageHeaderItemLinkCollection ribbonPageHeader) { Guard.ArgumentNotNull(ribbonPageHeader, "ribbonPageHeader"); this.ribbonPageHeader = ribbonPageHeader; }

/// <summary> /// See <see cref="UIElementAdapter{TUIElement}.Add(TUIElement)"/> /// for more information. /// </summary> protected override BarItem Add(BarItem uiElement) { Guard.ArgumentNotNull(uiElement, "uiElement"); if (ribbonPageHeader == null) throw new InvalidOperationException();

ribbonPageHeader.Add(uiElement); return uiElement; }

/// <summary> /// See <see cref="UIElementAdapter{TUIElement}.

Page 27: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 27 of 33

/// Remove(TUIElement)"/> for more information. /// </summary> protected override void Remove(BarItem uiElement) { { Guard.ArgumentNotNull(uiElement, "uiElement"); if (ribbonPageHeader == null) throw new InvalidOperationException();

ribbonPageHeader.Remove(uiElement); } } }}

The code for initializing the controls in the application shell form: internal ApplicationMenu ApplicationMenu { get { return applicationMenu; } }

internal RibbonPageHeaderItemLinkCollection RibbonPageHeader { get { return Ribbon.PageHeaderItemLinks; } }

Registering these UIElementAdaptors is done with: RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.ApplicationMenu, new ApplicationMenuUIAdapter(this.Shell.ApplicationMenu)); RootWorkItem.UIExtensionSites.RegisterSite( UIExtensionSiteNames.RibbonPageHeader, new RibbonPageHeaderUIAdapter(this.Shell.RibbonPageHeader));

There may be more things that could be done with the UIElementAdaptors but this is agood start.

dlx

Page 28: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 28 of 33

Nov 17, 2007

Attempting to Abstract the UI Even More

The past couple of weeks I have been thinking about abstracting the UI even more. If I cansuccessfully abstract for both a Ribbon and standard menu paradigms, why can’t I extend thatto handle the standard Microsoft menu system and other control sets? In this scenario eachof the possible shell applications and shell forms would be in separate projects. If theDeveloper Express controls were not available then it is a simple matter to remove theprojects which depend on these controls. It was an interesting thought exercise, but in theend the application would be restricted to the lowest common denominator for the UI. Thegoal of my project is to see if the CAB could be used to create a rich user interface. So Iabandoned the majority of this thought exercise.

I did implement one section to further abstract the UI. The ShellMain and UxExtensionservice now use Reflection to determine which interface paradigms were available and tofurther abstract the invocation of the selected paradigm. In previous versions of the ShellAppI would read the boolean app setting “IsRibbonForm”. IsRibbonForm would determine ifthe ShellRibbonApplication or the ShellApplication was launched. But as a boolean it wouldnever handle more than these two options.

In short, the solution is to decorate descendants of SmartClientApplication with an attribute(ShellApplicationAttribute). We also separate the Ribbon and menu paradigms in theUxExtension service and decorate the extracted classes with an attribute(UxExtensionAttribute). The attribute code is: [AttributeUsage(AttributeTargets.Class)] public class ShellApplicationAttribute : Attribute { public ShellApplicationAttribute(string name, string description) { this.name = name; this.description = description; }

private string name; public string Name { get { return name; } }

private string description; public string Description { get { return description; } } }

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] public class UxExtensionAttribute : Attribute {

Page 29: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 29 of 33

public UxExtensionAttribute(string name) { this.name = name; }

private string name; public string Name { get { return name; } } }

The name property of each of the attributes is used to determine which UI paradigm to use.This name property is read from the application configuration file. The shell assembly willthen be searched for a class decorated with the ShellApplicationAttribute with the namefrom the configuration file. The ShellMain class will then invoke the run method of theclass. Here is the new code for ShellMain:namespace DevLynx.ShellApp.Infrastructure.Shell{ using System; using System.Configuration; using System.Collections.Specialized; using System.Reflection; using DevLynx.ShellApp.Infrastructure.Interface; using System.Collections.Generic; using System.Text;

/// <summary> /// Main application entry point class. /// </summary> class ShellMain { /// <summary> /// Application entry point. /// </summary> [STAThread] static void Main() { DevExpress.UserSkins.BonusSkins.Register(); DevExpress.UserSkins.OfficeSkins.Register(); DevExpress.Skins.SkinManager.Default.

RegisterAssembly(typeof(DevExpress.UserSkins.Office2007Bonus).Assembly);

NameValueCollection appSettings =ConfigurationManager.AppSettings;

SetEnableFormSkins(appSettings);

#if (DEBUG) string modeMethod = "RunInDebugMode";#else // (RELEASE) string modeMethod = "RunInReleaseMode";#endif // if (DEBUG)

MethodInfo runMethod = GetApplicationType(appSettings).GetMethod(modeMethod,BindingFlags.NonPublic

Page 30: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 30 of 33

| BindingFlags.Public | BindingFlags.Static); if (runMethod == null) { // TODO: Log this exception throw new InvalidOperationException(

"Could not get the " + modeMethod + " method to launch the application.");

} runMethod.Invoke(null, null); }

private static Type GetApplicationType(NameValueCollection appSettings)

{ // We have decorated the ShellApplication classes

// with the ShellApplicationAttribute. So now find the// ShellApplicationAttribute with the corresponding// UxExtensionType name. We will then call the run method// of that class. This gives us some flexibility to

// add more ShellApplications with different UI components// when we need them. If we want to add a ShellApplication// from a different assembly than// DevLynx.ShellApp.Infrastructure.Shell we will have to// interogate other assemblies for the // ShallApplicationAttribute.

// Store all of the possible values for UxExtension so that we// can display the valid values in an error message.

List<string> acceptableUxExtensionTypes = new List<string>();

string uxExtensionType = appSettings.Get("UxExtensionType"); Type applicationType = null; // Now get all of the classes that are in this assembly.

// We will iterate through each class and get any// ShellApplicationAttribute that is decorating the class.

// When we find the attribute with the UxExtensionType from // the configuration file we store it for return to the// calling method.

System.Type[] existingClasses=Assembly.GetExecutingAssembly().GetTypes();

foreach (Type type in existingClasses) { object[] classAttributes = type.GetCustomAttributes(

typeof(ShellApplicationAttribute), false); if (classAttributes != null && classAttributes.Length > 0) { string name = (classAttributes[0]

as ShellApplicationAttribute).Name; // store in case we have to throw the exception below

acceptableUxExtensionTypes.Add(name); if (String.Compare(name, uxExtensionType, true) == 0)

{ applicationType = type; } } }

Page 31: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 31 of 33

// if we didn't find a class that has been decorated with// the appropriate attribute and name then we are done.// Throw an exception and get out.

if (applicationType == null) { //TODO: log the exception StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("There is an invalid UxExtensionType

value in your configuration file. Current value: "); stringBuilder.Append(uxExtensionType); stringBuilder.Append(". Valid values are: "); foreach (string value in acceptableUxExtensionTypes) { stringBuilder.Append(value); stringBuilder.Append(" "); } throw new

InvalidOperationException(stringBuilder.ToString()); } return applicationType; }

private static void SetEnableFormSkins(NameValueCollection appSettings)

{ bool enableFormSkins = false; try { string formSkins = appSettings.Get("EnableFormSkins"); enableFormSkins = Convert.ToBoolean(formSkins); } catch (FormatException ex) { // TODO: log this format error will use ex at that time // If this is a format error then the EnableFormSkins

// app setting is probably not either "true" or "false". // Eat the exception and keep the default set above.

} if (enableFormSkins) DevExpress.Skins.SkinManager.EnableFormSkins(); } }}

Similar code is used to determine which UxExtension class we want to instantiate.

dlx

Page 32: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 32 of 33

Nov 19, 2007Status Update and Reality Check

I started this investigation into CAB about two months ago now and it’s time to review theprocess and determine if CAB is still meeting the objectives.

First, as so many people have already said, there is a steep learning curve to CAB and theSCSF. I have been working with it for two months and I have only scratched the surface.Granted, I have only been developing in my spare time; but I still do not have a completegrasp of how all of the pieces fit together yet. This work will be time well spent if we do usethe CAB for the big rewrite at the day job. It’s too complex for my group to figure outwithout one person already having knowledge of the system.

There is no built-in ability to control order when adding items to the UIExtensionSites. Inthe current iteration of my test program the application exit command is always the firstcommand in the menu; very non-standard. David Platt, in his book Programming MicrosoftComposite UI Application Block and Smart Client Software Factory, describes one way tohandle this problem: 1) Create an event to which all modules subscribe. 2) Some loopingmechanism fires that event with the name of a module as a parameter. 3) When a modulesees its name it adds its UI elements.

Can we say kludge? I think that the CAB should have accounted for this functionality.Unless I can think of a better way, I’ll be using a variation of this method - but I don’t like it.

The CAB creates loosely coupled modules that are composed into a rich application.Through guidance packages, the SCSF extends the functionality of the CAB. It makesperfect sense that a development team will create modules of an application in separatesolutions. So why is it that you cannot create a Business Module in a separate solution fromthe shell? That’s right, you get an exception when running the Add Business Module recipe.The wording is: “An error happened while calling the value provider or evaluating the defaultvalue of argument ShellProject.” There is no reason for this restriction. I have a temporarysolution with a shell where I create Business Modules. I then copy the code verbatim into myactual project; the code works without modification. The option to add a View is not evenavailable in a solution without a shell. What the...? This is a gross oversight. Eventually I’llcreate Visual Studio templates to add modules and views - I shouldn’t have to.

And yet, there is power here. Decoupling to this degree is the right way to develop. Will itwork for me and my crew in the big rewrite? I don’t know yet. But it’s worth furtherinvestigation; and I’m having a blast learning a new technology. This is why I love my job!

dlx

Page 33: DevLynx Blog - CABs3.amazonaws.com/DevLynx/LiveJournal Blog.pdf · DevLynx Blog - CAB Echoes from the Cave Oct 9th 2007 ... Will need to add a ... 5. vCard support 6. Be able to backup

Page 33 of 33

Dec 2, 2007Status Update

It’s been two weeks since my last entry. I have been busy with the day job as well as otherpersonal matters. So, I have not had a great deal of time to contribute to the project. Some ofthe issues that I have worked on:• Removing the Source/Infrastructure directories in the ShellApp. Since the ShellApp is

going to be shared and no other modules placed into the solution it seemed wrong tohave the additional directory levels. So I flattened out the directory structure.

• In the previous released source of the ShellApp I had placed our standard copyrightnotice from the day job. This has been replaced with the MIT License so that others canlegally use my additions.

• Added structure to centralize some of the common command images that are invariablyin most projects. This way when someone demands something silly like “We mustchange the ‘cut’ command image to a bloody butcher’s knife?” it can be done in allprojects at once.

• Added command overlay images. At work I always seem to be the one who createscommand images. I know, we should get a graphic artist to do this - and actually we haveaccess to one now! I have spent untold hours creating various images for our commands(they were adequate but I am not an artist). One of the things that takes so much timeare what I call overlays. An overlay is a small command image modifier such as a plussign for ‘add’. For instance a ‘Project’ command will have modifiers for add, delete, edit,import, export, make it spin around on it’s head three times, etc. I always do this with abase image and the modifier is placed in the lower right hand corner. When someoneasked for an overlay replacement I would have to go in and change many commandimages. Now overlays can be placed on the image automatically, are easy to change, andare consistent across commands and projects.

• Created automated builds using FinalBuilder.• I have set up FogBugz to use as an issue tracking system and have started to track issues.

In other words, I have not been completely idle. My next step is also going to be morestructure. I will be adding unit tests and doing code coverage. So it probably won’t be untilafter the holidays that I have more features implemented.

dlx