Moving an External Command Button within the Ribbon

I recently presented Frode Tørresdal’s
unofficial custom ribbon button implementation
showing how you can add a custom button that Revit knows nothing about to the ribbon and hook it up with the Idling event to perform some action requiring access to the Revit API.

It would be much simpler, safer and more effective if we could implement a normal official Revit external command instead and create a button anywhere we want in the Revit ribbon to drive that.

Well, happily, we can, almost, as Scott Wilson points out in his
comment on that post.

The Living

Before getting to that, though, let me mention something of interest to architects, techies and all AEC visionaries alike, the acquisition of the very forward-thinking architectural design studio
The Living,
headed by David Benjamin.
It is the latest addition to the Autodesk research network, the first of its kind Autodesk Studio, in a move which Benjamin says ‘will enable The Living to do more of what we are already doing and super-charge it.’

Here is the official
announcement and
articles about it by
Architect,
ArchDaily and
Architectural Record.

Scott

I thought I’d let you know of something I found a little while ago while participating in the Revit API forum thread on
adding a button to an existing ribbon panel.

Basically, there is a way of adding your own custom panels and buttons to the system tabs and have them behave as standard command buttons with full API access, no need for Idling callbacks and other stuff.

Here is my brief explanation from the thread: “If you add your command button using standard API methods to its own ribbon panel on either your own tab or the built in add-ins tab, you can then find the panel using the above methods and insert it into any of the system tabs using the Autodesk.Windows.RibbonTab.Panels.Add method. The button will then behave in the same way as it would on the original tab and the API doesn’t seem to notice – to the extent of my brief testing, anyway. You can then remove the panel from the add-ins tab if desired and the panel will remain happily in its new home.”

I dug up my test code, cleaned it up into a little demo project and posted it to Dropbox.com in
RibbonMoveExample.zip.

Please see the comments included in the source code below for the rest of Scott’s detailed explanation.

Jeremy

Jeremy adds: wow, very cool!

What a beautiful workaround, working around the official Revit API limitations and yet retaining all the official functionality goodness – much better than the completely unofficial hack using the .NET UI Automation library.

I compiled and tested this.

It creates a new panel containing a new button and moves them to the Revit ribbon Manage tab.
Here is the result:

Moving an external command ribbon buttonIt supports all the standard external command functionality, e.g. an external command availability class.

External Command for Testing

The external command used for testing is defined as follows:

using System;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;

namespace RibbonMoveExample
{
    public class ExampleCommand_Availability
        : IExternalCommandAvailability
    {
        public bool IsCommandAvailable(
            UIApplication uiApp,
            CategorySet categorySet )
        {
            Autodesk.Revit.UI.UIDocument uiDoc
                = uiApp.ActiveUIDocument;

            if( uiDoc != null )
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }


    [Transaction( TransactionMode.Manual )]
    public class ExampleCommand : IExternalCommand
    {
        public Autodesk.Revit.UI.Result Execute(
            ExternalCommandData commandData,
            ref String message,
            ElementSet elements )
        {
            try
            {
                Document dbDoc = commandData.Application
                    .ActiveUIDocument.Document;

                TaskDialog.Show( "ExampleCommand",
                    "Current document title: "
                    + dbDoc.ProjectInformation.Name
                    + "nLocation: " + dbDoc.PathName );
            }
            catch( Exception e )
            {
                message = e.Message;
                return Result.Failed;
            }
            return Result.Succeeded;
        }
    }
}

External Application Implementation

As said, the external application sports Scott’s interesting explanations, including a detailed dissection of the ribbon panel and ribbon item Id property values required for identification:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;

using Autodesk.Revit.ApplicationServices;
using Autodesk.Revit.UI;

using adWin = Autodesk.Windows;

namespace RibbonMoveExample
{
    /// <summary>
    /// Demonstrates the use of Autodesk.Windows.RibbonControl
    /// to move API created ribbon buttons and / or panels from
    /// their original location to any other tab / panel including
    /// system created ones.
    /// I'm pretty sure that this technique would also work in
    /// reverse to allow grabbing system panels / buttons and adding
    /// them to API created Tabs and even other system tabs, meaning
    /// that some drastic customisation of Revit's ribbon menu is
    /// possible.
    /// Revit CUI addin anybody? :)
    ///
    /// Created by Scott Wilson, released into the public domain as
    /// demonstration of concept only. Use at own risk.
    /// </summary>
    public class RibbonMoveExampleApp : IExternalApplication
    {
        // System tab and panel ids to use as targets for the
        // demo to find other valid ids for system tabs and
        // panels, simply dump out the id of each item visited
        // in the foreach loops below

        String SystemTabId = "Manage";
        String SystemPanelId = "settings_shr";

        // Example names of API created tabs, panels and buttons
        // To try this demo on ribbon items already created by
        // another loaded addin, enter the appropriate details
        // here and comment out everything in the OnStartup method
        // except for the ApplicationInitialized registration

        String ApiTabName = "New Tab";
        String ApiPanelName = "New Panel";
        String ApiButtonName = "NewButton";
        String ApiButtonText = "NewnButton";

        public Result OnStartup(
            UIControlledApplication uiConApp )
        {
            // Add our own tab, panel and command button
            // to the ribbon using the API

            uiConApp.CreateRibbonTab( ApiTabName );

            RibbonPanel apiRibbonPanel = uiConApp.CreateRibbonPanel(
                ApiTabName, ApiPanelName );

            PushButtonData buttonDataTest = new PushButtonData(
                ApiButtonName, ApiButtonText,
                Assembly.GetExecutingAssembly().Location,
                "RibbonMoveExample.ExampleCommand" );

            buttonDataTest.AvailabilityClassName
                = "RibbonMoveExample.ExampleCommand_Availability";

            apiRibbonPanel.AddItem( buttonDataTest );

            // Subscribe to the "ApplicationInitialized" event
            // then continue from there once it is fired.
            // This is to ensure that the ribbon is fully
            // populated before we mess with it.

            uiConApp.ControlledApplication.ApplicationInitialized
                += OnApplicationInitialized;

            return Result.Succeeded;
        }


        void OnApplicationInitialized(
            object sender,
            Autodesk.Revit.DB.Events.ApplicationInitializedEventArgs e )
        {
            // Find a system ribbon tab and panel to house
            // our API items
            // Also find our API tab, panel and button within
            // the Autodesk.Windows.RibbonControl context

            adWin.RibbonControl adWinRibbon
                = adWin.ComponentManager.Ribbon;

            adWin.RibbonTab adWinSysTab = null;
            adWin.RibbonPanel adWinSysPanel = null;

            adWin.RibbonTab adWinApiTab = null;
            adWin.RibbonPanel adWinApiPanel = null;
            adWin.RibbonItem adWinApiItem = null;

            foreach( adWin.RibbonTab ribbonTab
                in adWinRibbon.Tabs )
            {
                // Look for the specified system tab

                if( ribbonTab.Id == SystemTabId )
                {
                    adWinSysTab = ribbonTab;

                    foreach( adWin.RibbonPanel ribbonPanel
                        in ribbonTab.Panels )
                    {
                        // Look for the specified panel
                        // within the system tab

                        if( ribbonPanel.Source.Id == SystemPanelId )
                        {
                            adWinSysPanel = ribbonPanel;
                        }
                    }
                }
                else
                {
                    // Look for our API tab

                    if( ribbonTab.Id == ApiTabName )
                    {
                        adWinApiTab = ribbonTab;

                        foreach( adWin.RibbonPanel ribbonPanel
                            in ribbonTab.Panels )
                        {
                            // Look for our API panel.
                            // The Source.Id property of an API created
                            // ribbon panel has the following format:
                            // CustomCtrl_%[TabName]%[PanelName]
                            // Where PanelName correlates with the string
                            // entered as the name of the panel at creation
                            // The Source.AutomationName property can also
                            // be used as it is also a direct correlation
                            // of the panel name, but without all the cruft
                            // Be sure to include any new line characters
                            // (n) used for the panel name at creation as
                            // they still form part of the Id & AutomationName

                            //if(ribbonPanel.Source.AutomationName
                            //  == ApiPanelName) // Alternative method

                            if( ribbonPanel.Source.Id ==
                                "CustomCtrl_%" + ApiTabName + "%" + ApiPanelName )
                            {
                                adWinApiPanel = ribbonPanel;

                                foreach( adWin.RibbonItem ribbonItem
                                    in ribbonPanel.Source.Items )
                                {
                                    // Look for our command button

                                    // The Id property of an API created ribbon
                                    // item has the following format:
                                    // CustomCtrl_%CustomCtrl_%[TabName]%[PanelName]%[ItemName]
                                    // Where ItemName correlates with the string
                                    // entered as the first parameter (name)
                                    // of the PushButtonData() constructor
                                    // While AutomationName correlates with
                                    // the string entered as the second
                                    // parameter (text) of the PushButtonData()
                                    // constructor
                                    // Be sure to include any new line
                                    // characters (n) used for the button
                                    // name and text at creation as they
                                    // still form part of the ItemName
                                    // & AutomationName

                                    //if(ribbonItem.AutomationName
                                    //  == ApiButtonText) // alternative method

                                    if( ribbonItem.Id
                                        == "CustomCtrl_%CustomCtrl_%"
                                        + ApiTabName + "%" + ApiPanelName
                                        + "%" + ApiButtonName )
                                    {
                                        adWinApiItem = ribbonItem;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // Make sure we got everything we need

            if( adWinSysTab != null
                && adWinSysPanel != null
                && adWinApiTab != null
                && adWinApiPanel != null
                && adWinApiItem != null )
            {
                // First we'll add the whole panel including
                // the button to the system tab

                adWinSysTab.Panels.Add( adWinApiPanel );

                // now lets also add the button itself
                // to a system panel

                adWinSysPanel.Source.Items.Add( adWinApiItem );

                // Remove panel from original API tab
                // It can also be left there if needed,
                // there doesn't seem to be any problems with
                // duplicate panels / buttons on seperate tabs
                // / panels respectively

                adWinApiTab.Panels.Remove( adWinApiPanel );

                // Remove our original API tab from the ribbon

                adWinRibbon.Tabs.Remove( adWinApiTab );
            }

            // A new panel should now be added to the
            // specified system tab. Its command buttons
            // will behave as they normally would, including
            // API access and ExternalCommandAvailability tests.
            // There will also be a second copy of the command
            // button from the panel added to the specified
            // system panel.
        }

        public Result OnShutdown( UIControlledApplication a )
        {
            return Result.Succeeded;
        }
    }
}

Many thanks to Scott for this very creative workaround!

Use at Your Own Risk

By the way, please note our standard
disclaimer.

The Revit API development team have good reasons for limiting the places that an add-in can place its command buttons.

You work around those limitations at your own risk.

This functionality may very well be disabled at any time with no warning.


Comments

6 responses to “Moving an External Command Button within the Ribbon”

  1. Wow this is interesting. Over the years I have had many requests by people if we could disable the explode command. Now I’m assuming we could remove that button altogether from the ribbon? I wonder how this works on contextual panels???

  2. Dear Phillip,
    Power to the people, or at least to you.
    Cheers, Jeremy.

  3. Scott Wilson Avatar
    Scott Wilson

    Yes you can indeed remove the explode buttons (I tried it out) and they stay removed for the remainder of the Revit session. The trick is to catch them while they are showing, I did it by selecting an imported cad file to bring up the context panel and then ran an external command that searches for the buttons and removes them, it’s a manual process though.
    For automation I think it would be possible to place the seek and remove code into an IExternalCommandAvailability class associated with an API button that has been added to the modify tab. It would then get triggered automatically along with the display of the contextual panel. The button could then even remove itself once it has done its job (the perfect crime).
    There’s probably a race condition though, as the explode buttons might not always be added before the availability checker is triggered, but its worth a try.

  4. Gamal Kheder Avatar
    Gamal Kheder

    Dear Jeremy
    I have a question in another issue and i need your advise :)
    How to tag element in a linked project ?
    I use the this method to tag a linked pipe element but it throw an error please advise thanks :)
    LocationCurve loc = pipe.Location as LocationCurve;
    XYZ startp = loc.Curve.GetEndPoint(0);
    XYZ endp = loc.Curve.GetEndPoint(1);
    XYZ midp = loc.Curve.Evaluate(0.5, true);
    newtag = DOC.Create.NewTag(view, pipe, false, tagmode, tagorian, midp);

  5. I’ve got some code working based on this article. The code loops through an xml file and sets ribbonItem.IsVisible per button in my addin. This works for all of my buttons except for a PullDownButton. The buttons that are added to the PullDownButton aren’t included as items in RibbonPanel.Source.Items. Any idea how to get the buttons in the PullDownButton that is found in there?
    Thanks

  6. @ TroyGates. If the button is PullDownButton you should be able to cast it to a PullDownButton. Then use the GetItems() method to get an IList that you can then iterate through to set the visibility. I’ve implemented this in an addin before.

Leave a Reply to Phillip MillerCancel reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading