DCSIMG
When The Whole is Greater Than The Sum of Its Parts - IronShay

When The Whole is Greater Than The Sum of Its Parts

I did a lecture earlier this week about the design behind VSTO 3.0 Ribbon. This is the follow-up post with detailed information and a complete demo.

Presentation: download
Demo project: download
In order to run the demo, open it in VS2008, build the setup project and install it, then run Word 2007.

How to extend the Office 2007 Ribbon?

In order to define a custom ribbon, you should construct an xml file that describes the controls you want to present there.
For example, a ribbon xml file can be like the following:

<customUI onLoad="Ribbon_Load" xmlns="http://schemas.microsoft.com/office/2006/01/customui">

    <ribbon>

        <tabs>

            <tab idMso="TabAddIns">

                <group id="group1" label="group1">

                    <button id="myButton" onAction="myButton_Click" getLabel="GetLabel" showImage="false" />

                </group>

            </tab>

        </tabs>

    </ribbon>

</customUI>

This configures a single ribbon, tab, group and a button. You can also see here some bold text, which emphasizes the callback method names that one can define on this configuration file.

The next step is to create an Office add-in, this can be a VSTO add-in, shared add-in or any other add-in that will work. For example, in order to create a Shared Add-in project, Open VS, click on File->New->Project. Then go to "Other project types"->Extensibility and choose "Shared Add-in". A quick wizard will guide till you can start writing your add-in code.
Now, all our add-in needs to do, is to implement IRibbonExtensibility and return the xml we've mentioned above. For instance:

#region IRibbonExtensibility Members

 

public string GetCustomUI(string ribbonID)

{

    return GetResourceText("Ribbon.xml");

}

 

#endregion

Here I assume that the xml file is stored in a resource named "Ribbon.xml" and that I have a method named "GetResourceText" that returns the file contents.

When Office loads the add-in, it calls the callbacks that was defined on the xml using reflection. This is an important part for the next parts of this article.
Sample implementation can be:

public string GetLabel(IRibbonControl control)

{

    return "Click Me!";

}

Refreshing the ribbon is done by calling the ribbon's Invalidate and InvalidateControl methods.

Take a look at the presentation, where you can see the flow between Office and the Add-in.

What Happened on VSTO 3.0?

If you second look on the last part, or worst - if you try to develop applications that way, you'll figure out pretty quickly that this is not our winning horse. You need to master xml files, various interfaces and you need to design the UI without seeing it until you execute your add-in for the first time.

VSTO 3.0 for Visual Studio 2008 took all of that into consideration - you don't have to worry about xml anymore, you don't have to implement interfaces and you even have a full designer support.

Key Concepts of VSTO 3.0 Ribbon

Just to make clear - no changes were made to Office before and after VSTO 3.0. Nothing was added in order to support it. VSTO 3.0 wraps the xml, callbacks and interfaces with developer-friendly classes and adds a full designer support.
This is a great thing, and it's even better if you think that it uses current abilities in order to take them to the next level - sound like we're experiencing the evolution right before our eyes!
So how did they do that?

  1. Automatic XML generation - you design the ribbon using a designer and the ribbon XML will be automatically created on run time.
  2. Generalizing method invocation - the ribbon XML contains a large amount of callbacks to define properties. For example - getLabel, getEnabled, getVisible and so on. It'll be more logical if these were exposed as real properties . This is what has been done here by implementing IReflect's invoke method.
  3. Automatically refresh the ribbon when needed - when a property is changed, the control is invalidated so the Ribbon can refresh itself.
  4. Full designer support - generating the xml on runtime led to the opportunity to give a designer support. You design on design time and your design is converted to the needed xml on runtime. Brilliant!

Demo

In the demo I've built a very small and targeted replica of the VSTO 3.0 Ribbon classes.
The demo persists of 3 projects -
* Ribbon - a class library that simulates the VSTO 3.0 ribbon class.
* SharedAddinDemo - a shared add-in project that extends Word applications (created using the Shared Add-in project type).
* SharedAddinDemoSetup - a setup project for the add-in that was added automatically after I had added the shared add-in project.

I'll go over the bullets from the last section and explain how they reflect in the demo.

Automatic XML Generation

I've created a base abstract class named BaseXmlWritingControl. Every ribbon control implements it. It has 4 methods: WriteStartXml, WriteXmlAttributes, WriteChildrenXml and WriteXml.
The 3 first methods are self-explanatory, WriteXml just gathers them all together and write an end element as well.
This way, if I call the ribbon's WriteXml, it'll generate the entire ribbon xml automatically (because WriteChildrenXml will go deeper and write the xml of the children and the children of the children and so on).

The shared add-in implements the IRibbonExtensibility interface and uses WriteXml to create the ribbon's xml on runtime:

#region IRibbonExtensibility Members

 

public string GetCustomUI(string RibbonID)

{

    return manager.RibbonXml;

}

 

#endregion

manager is the RibbonManager class which exposes the needed methods and services to the add-in project.

Generalizing Method Invocation

We have a finite amount of "events" that the ribbon xml gives us. This means that we can set the same callback method for all of the controls. When the callback is called, all we have to do is to find out which control is making the call and act accordingly.

I didn't go that deep during my presentation though. I only wanted to show how you can generalize the method invocation. I leave it to you to take this concept to the next level.

So what did I do there? The shared add-in project contains a CustomConnect.cs file. This is a partial class of the Connect class (only for convenience reasons). There I implement the IReflect interface.
We've said earlier that Office executes the callback methods using reflection. By implementing IReflect, we can control what will be executed when Office tries to invoke a certain callback. The IReflect invoke method will redirect calls from Office to different and more developer-friendly methods.

There is no need to implement all of the IReflect methods and properties. The most important methods for us are Invoke and GetMethods. GetMethods returns all of the method signatures to the caller. If a method does not appear in the returned list, Office won't even bother to send an Invoke request to the add-in.
Invoke redirects the call to the manager, giving it the name of the method to invoke and the args array. The manager then decided, according to the method name, which method to call.
The manager Invoke implementation (a very naive implementation):

public object Invoke(string name, object[] args)

{

    if (name.Contains("Loaded"))

    {

        officeRibbon.RibbonLoaded(args[0] as IRibbonUI);

 

        return null;

    }

 

    if (name.Contains("Label"))

    {

        return officeRibbon.Tab.Group.Button.GetLabel(null);

    }

 

    if (name.Contains("Button"))

    {

        officeRibbon.Tab.Group.Button.ButtonClick(null, null);

 

        return null;

    }

 

    return null;

}

Refreshing the Ribbon Automatically

When the ribbon is loaded, the onLoad event is raised. This event sends us an IRibbonUI object. This object has only 2 methods - Invalidate and InvalidateControl(string controlId). Calling Invalidate, refreshes the whole ribbon, InvalidateControl refreshes only the given control.
In order to save the ribbon object, I've added a RibbonLoaded method to the OfficeRibbon class that gets called on the onLoad event. All it does is to save the IRibbonUI object for future use:

public void RibbonLoaded(IRibbonUI ribbon)

{           

    this.ribbon = ribbon;

}

When a property of a control is set, we need to call the InvalidateControl so the ribbon will be refreshed and the changes will reflect there. This is what is being done on the RibbonButton.Label property:

public string Label

{

    get { return label; }

    set

    {

        label = value;

        ParentRibbon.Invalidate();

    }

}

Full Designer Support

After you create classes with properties for the available control types, adding designer support is easier. Designer support is a whole subject for itself so I won't dive into it now.

Conclusions

VSTO 3.0 Ribbon implementation is simple but genius. They used current abilities in order to create a better platform - the whole is greater than the sum of its parts.

What I want you to remember from this long article is not related to VSTO and how cool it is, I want you to remember that sometimes you can take the current design and build above it instead of rewrite it all. I want you also to remember that you can create an xml file to configure your application and even include callback names there. This can give your application some great flexibility and extensibility features.

I hope you've been enlightened,
Shay.

Share this post :
Published Friday, June 27, 2008 6:12 PM by shayf

Comments

# re: When The Whole is Greater Than The Sum of Its Parts

Serious stuff...

Friday, June 27, 2008 6:15 PM by Guy Burstein

# When the whole is greater than the sum of its parts

You've been kicked (a good thing) - Trackback from DotNetKicks.com

Friday, June 27, 2008 6:27 PM by DotNetKicks.com

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above: