?

Log in

No account? Create an account
DevLynx

Ribbon Galleries (redux)

I have completed a first version of Ribbon Gallery support for the CAB DevExpress Extension Kit. My solution is different than the other UIAdapters in the extension kit because of the way Developer Express chose to implement galleries. Including gallery support in an application involves adding a Gallery to a Ribbon Page Group, then adding a Gallery Group to the Gallery and finally, adding a Gallery Item into the Gallery Group. It is in this final step that the major difference is revealed; the Tag property of the GalleryItem must be set to the CommandName to be run.

There are two reasons that the GalleryItem Tag property is required to be the CommandName. First, the Developer Express gallery implementation has the 'click' events in the RibbonGalleryBarItem not in the GalleryItem. That means that the individual GalleryItem cannot handle it's own click event. This leads to the second reason for placing the CommandName in the Tag. The CAB has, purposefully, highly decoupled the UIElement from the command handler. Within the command handler, you cannot obtain any information about the UIElement that called the command handler.

We end up with a conundrum; Developer Express does not allow us to link a GalleryItem directly to the click event and the CAB does not allow us to find information about which GalleryItem was clicked. My solution is to supply the CommandName within the GalleryItem Tag property. At one point I considered creating a GalleryItem descendant which would add a CommandName property. But since the CAB prevents information about UIElement from being passed to a command handler the Tag property could not be used for anything else. I determined that a GalleryItem descendant was overkill. I'm not entirely happy with this solution but I feel that it is an adequate compromise.

The other difference is that we need access to the WorkItem which added the command to be fired. This is passed into the RibbonGalleryUIAdapter during construction. The first version of the adapter stated that the RootWorkItem was to be passed. However, Karel Kral was kind enough to tell me that this wouldn't work. While the EventBroker stores all of it's events in the root work item, commands are stored in, and can only be accessed from, the work item that added the command. This actually didn't change any of the code flow, but I made sure that the parameter and property names were modified as well as the documentation. Thanks Karel!

The base solution contains two files RibbonGalleryUIAdapter.cs and RibbonGalleryGroupUIAdapter.cs.

RibbonGalleryUIAdapter.cs

using System;

using DevExpress.XtraBars;

using DevExpress.XtraBars.Ribbon;

using Microsoft.Practices.CompositeUI.UIElements;

using Microsoft.Practices.CompositeUI.Utility;

using Microsoft.Practices.CompositeUI;

using Microsoft.Practices.CompositeUI.Commands;

 

namespace CABDevExpress.UIElements

{

    /// <summary>

    /// An adapter that wraps a <see cref="RibbonGalleryBarItem"/> for use

    /// as an <see cref="IUIElementAdapter"/>.

    /// </summary>

    public class RibbonGalleryUIAdapter : UIElementAdapter<GalleryItemGroup>

    {

        private readonly RibbonGalleryBarItem ribbonGallery;

 

        /// <summary>

        /// Initializes a new instance of the

        /// <see cref="RibbonApplicationMenuUIAdapter"/> class.

        /// </summary>

        /// <param name="ribbonGallery">The application menu.</param>

        /// <param name="workItem">The work item which added the command.

        /// We need to access the Commands property of the work item to

        /// fire the Command associated with the GalleryItem.</param>

        public RibbonGalleryUIAdapter(RibbonGalleryBarItem ribbonGallery,

            WorkItem workItem)

        {

            Guard.ArgumentNotNull(ribbonGallery, "RibbonGalleryBarItem");

            Guard.ArgumentNotNull(workItem, "workItem");

 

            this.ribbonGallery = ribbonGallery;

            this.workItem = workItem;

            // Since Developer Express does not support click events for

            // GalleryItems we need to handle the ItemClick event here.

            this.ribbonGallery.Gallery.ItemClick +=

                new GalleryItemClickEventHandler(ItemClick);

        }

 

        private readonly WorkItem workItem;

        /// <summary>

        /// Gets the work item which was passed in at creation.

        /// </summary>

        /// <value>The work item.</value>

        protected WorkItem WorkItem

        {

            get { return workItem; }

        }

 

        /// <summary>

        /// Fired when any <see cref="GalleryItem"/> contained in this

        /// <see cref="RibbonGalleryBarItem"/> is clicked. If the

        /// GalleryItem.Tag property contains an active CommandName string,

        /// that command is fired.

        /// </summary>

        /// <param name="sender">The sender.</param>

        /// <param name="e">The <see cref="GalleryItemClickEventArgs"/>

        /// instance containing the event data.</param>

        protected virtual void ItemClick(object sender,

            GalleryItemClickEventArgs e)

        {

            if (e.Item.Tag is string

                && !String.IsNullOrEmpty(e.Item.Tag as string))

            {

                Command command = WorkItem.Commands.Get<Command>

                    (e.Item.Tag as string);

                if (command != null)

                    command.Execute();

            }

        }

 

        /// <summary>

        /// See <see cref="UIElementAdapter{TUIElement}.Add(TUIElement)"/>

        /// for more information.

        /// </summary>

        protected override GalleryItemGroup Add(GalleryItemGroup uiElement)

        {

            Guard.ArgumentNotNull(uiElement, "GalleryItemGroup");

 

            ribbonGallery.Gallery.Groups.Add(uiElement);

            return uiElement;

        }

 

        /// <summary>

        /// See <see cref="UIElementAdapter{TUIElement}.Remove(TUIElement)"/>

        /// for more information.

        /// </summary>

        protected override void Remove(GalleryItemGroup uiElement)

        {

            Guard.ArgumentNotNull(uiElement, "GalleryItemGroup");

 

            ribbonGallery.Gallery.Groups.Remove(uiElement);

        }

    }

}


RibbonGalleryGroupUIAdapter.cs

using System;

using Microsoft.Practices.CompositeUI.UIElements;

using DevExpress.XtraBars.Ribbon;

using Microsoft.Practices.CompositeUI.Utility;

 

namespace CABDevExpress.UIElements

{

    /// <summary>

    /// An adapter that wraps a <see cref="GalleryItemGroup"/> for use

    /// as an <see cref="IUIElementAdapter"/>.

    /// </summary>

    public class RibbonGalleryGroupUIAdapter : UIElementAdapter<GalleryItem>

    {

        private readonly GalleryItemGroup ribbonGalleryGroup;

 

        /// <summary>

        /// Initializes a new instance of the

        /// <see cref="RibbonGalleryGroupUIAdapter"/> class.

        /// </summary>

        /// <param name="ribbonGalleryGroup"></param>

        public RibbonGalleryGroupUIAdapter(GalleryItemGroup ribbonGalleryGroup)

        {

            Guard.ArgumentNotNull(ribbonGalleryGroup, "GalleryItemGroup");

            this.ribbonGalleryGroup = ribbonGalleryGroup;

        }

 

        /// <summary>

        /// See <see cref="UIElementAdapter{TUIElement}.Add(TUIElement)"/> for

        /// more information.

        /// </summary>

        protected override GalleryItem Add(GalleryItem uiElement)

        {

            ValidateUiElement(uiElement);

 

            ribbonGalleryGroup.Items.Add(uiElement);

            return uiElement;

        }

 

        /// <summary>

        /// Validates a UIElement to ensure that the GalleryItem.Tag property

        /// contains a non-empty string value. This value is assumed to be a

        /// CommandName associated with an instantiated CAB Command.

        /// </summary>

        /// <param name="uiElement">The UIElement to be validated.</param>

        protected virtual void ValidateUiElement(GalleryItem uiElement)

        {

            // not using Guard here so that we can supply the additional

            // information in the message.

            if (uiElement == null)

                throw new ArgumentNullException("uiElement.Tag cannot null."

                    + " It must contain the CommandName to be fired.");

            if (uiElement.Tag is string)

            {

                if (String.IsNullOrEmpty(uiElement.Tag as string))

                    throw new ArgumentException("uiElement.Tag cannot be empty."

                        + " It must contain the CommandName to be fired.");

            }

            else

                throw new ArgumentException("uiElement.Tag must be a string and"

                    + " must contain the CommandName to be fired.");

        }

 

        /// <summary>

        /// See <see cref="UIElementAdapter{TUIElement}.Remove(TUIElement)"/> for

        /// more information.

        /// </summary>

        protected override void Remove(GalleryItem uiElement)

        {

            Guard.ArgumentNotNull(uiElement, "GalleryItem");

            ribbonGalleryGroup.Items.Remove(uiElement);

        }

    }

}


An example of how galleries are created follows. Note that this example takes shortcuts with UIExtension site names; you will want to follow all of the standard conventions regarding UIExtension site names as constants.

// First we have to create the Ribbon Page and Group.

string pageName = "Galleries";

RibbonPage ribbonPage = new RibbonPage(pageName);

WorkItem.UIExtensionSites[UIExtensionSiteNames.RibbonControl].

    Add<RibbonPage>(ribbonPage);

WorkItem.UIExtensionSites.RegisterSite(pageName, ribbonPage);

string pageGroupName = "My Gallery Page Group";

RibbonPageGroup ribbonGroup = new RibbonPageGroup(pageGroupName);

ribbonGroup.ShowCaptionButton = false;

WorkItem.UIExtensionSites[pageName].Add<RibbonPageGroup>(ribbonGroup);

WorkItem.UIExtensionSites.RegisterSite(pageGroupName, ribbonGroup);

 

// Next we create the Gallery within the Ribbon Group

string galleryName = "Gallery";

RibbonGalleryBarItem ribbonGallery = new RibbonGalleryBarItem();

ribbonGallery.Caption = galleryName;

ribbonGallery.Gallery.AllowHoverImages = true;

ribbonGallery.Gallery.ColumnCount = 3;

ribbonGallery.Gallery.ImageSize = Resources.Contact24.Size;

ribbonGallery.Gallery.HoverImageSize = Resources.Contact64.Size;

WorkItem.UIExtensionSites[pageGroupName].Add<BarItem>(ribbonGallery);

// Pass the WorkItem which added the command into the

// RibbonGalleryUIAdapter. This allows the command to be fired.

WorkItem.UIExtensionSites.RegisterSite(galleryName,

    new RibbonGalleryUIAdapter(ribbonGallery, WorkItem));

 

// Then create the group within the gallery

string galleryGroupName = "Gallery Group";

GalleryItemGroup galleryGroup = new GalleryItemGroup();

galleryGroup.Caption = galleryGroupName;

WorkItem.UIExtensionSites[galleryName].Add<GalleryItemGroup>(galleryGroup);

WorkItem.UIExtensionSites.RegisterSite(galleryGroupName,

    new RibbonGalleryGroupUIAdapter(galleryGroup));

 

// Finally create the gallery item and add it to the gallery group.

GalleryItem galleryItem = new GalleryItem();

galleryItem.Caption = "Gallery Item";

galleryItem.Image = Resources.Contact24;

galleryItem.HoverImage = Resources.Contact64;

galleryItem.Hint = galleryItem.Caption;

// Set the galleryItem's tag to the name of the command to fire.

galleryItem.Tag = CommandNames.ContactsOpen;

WorkItem.UIExtensionSites[galleryGroupName].Add<GalleryItem>(galleryItem);


As with the last Ribbon UIAdapters that I developed, the folks over at the CAB DevExpress Extension Kit are free to incorporate these adapters into the extension kit.

However, within this whole concept there is one major piece missing. The CAB requires each UIElement to have it's own command. What if you want elements which are dynamic such as Developer Express skins, fonts, printers, etc? Creating CommandHandler's dynamically just isn't practical. In the CAB DevExpress Extension Kit Quick Start project the skins menu is created dynamically by completely bypassing the CAB; adding the BarButtonItems directly to the menu and handling the ItemClick event. This works very well but, in my opinion, violates the CAB philosophy — I'm uncomfortable going there. More on this later.

dlx

Comments

RE "the skins menu is created dynamically by completely bypassing the CAB"
You're right, but the inflexibility that CAB can create by being so decoupled, sometimes makes the process of creating menus difficult to impossible to get exactly right.
I've learned that to violate CAB every now and then is a good thing.

I would say, don't bother using CAB commands if a particular scenario doesn't need the decoupling - it can be a lot of effort for no gain. The skin menu is a perfect example, none of it needs or benefits from anything CAB commands has.
Sorry it took me so long to get back to your comment. I was gone for a couple of days and then had a heck of a time catching up.

I absolutely agree with your comment that violating CAB on occasion would be the only way to handle a difficult situation. My goal in this project is to see if I can follow the CAB philosophy and still create a rich UI. If I find something that I can't do, then I will certainly go outside of the paradigm.

The nature of my project, where I support both menu/toolbar and ribbon controls, forces me to think about generalizing UIElement creation. It would be difficult for me handle both paradigms if I created UIElements bypassing the CAB commands. Also, it is nice to have the tools to accomplish these tasks in a CAB specific way. This give developers the choice to use these tools if the want or bypass the CAB.

Thanks for commenting,

dlx