UI Automation, Ribbon Panel Item Access, the ItemExecuted Event and Vacation

Here is an interesting exploration of using UI Automation to traverse the Revit ribbon items and subscribe to an event enabling you to determine when certain commands are executed.

I'll leave you to ponder this while I take a vacation in the snow next week.

Happy Revit add-in programming!

Wildhaus and the Schafberg

Question: I would like to keep track of all add-ins that are started on a given Revit installation. We currently keep track of all our own using specific routines in the add-ins. But is there a way to keep track of the add-ins that we did not create ourselves? Like an event that fires after finishing or before starting? I guess we could look in the journals, but is there a simpler way?

Answer: Just as you say and do yourself, keeping track of add-in activity requires special instrumentation, either in the add-in itself or somewhere within Revit.

My first impulse when I started reading your query was to suggest using the journal file, which does exactly what you wish. In the end, you mention it yourself.

I cannot really think of any better method and have heard now and then that various applications do indeed make use of the journal file for these kind of tracking activities.

Response: I implemented a journal file scanner and it has been running for some time now, but the results are not reliable because they depend on the users keeping their journal files.

I would like something a little more robust. Therefore I turn to the add-in command binding. Is there a way to bind to external commands? And if so: how can I find the correct ID for the external command?

I figured that I can map all the registered commands when Revit has initialized and subscribe to the .BeforeExecute event of them to log when they are used.

Answer: That sounds like a brilliant idea!

Yes, of course you can determine the ID for external commands, and here is a dedicated blog post on launching a custom add-in command.

Please let us know whether that provides what you need.

I think that your requirement and your idea for addressing it are definitely of general interest to the community and generic enough to warrant a blog post, so I would love to implement and publish this, if that is OK, assuming it works.

Response: OK, well that's good.

I read through your blog post.

Very good, some follow-ups:

  1. For this, I need to know the ribbon panel tab name for the external commands. I have not yet found a way to get that for ones I have not created myself. Is this possible through the API?
  2. The name string is generated by the sequence of controls that one has to navigate to the control, and your string represents tab > panel > button. What scenarios could we encounter? A pull-down button would give us one more step, right? Anything else you can think of?

Later...

I've been exploring the workflow suggested by Scott Wilson in one of your blog posts on ribbons.

I only have one question: where does one get the Autodesk.Window namespace? It's not in any of the standard API-references or is it something I have not seen yet?

Later...

I found it: looked at your ModifyTabButton on GitHub. It's in the AdWindows.dll assembly.

All right, back to the digging!

Answer: Congratulations on making rapid progress.

To answer your questions:

1. Yes, you can get all the tab names by iterating through the panels and their tabs, I think.

Look at the UIApplication.GetRibbonPanels method, start from there, and research what other objects you can traverse from that staring point.

You may even be able to look through them using RevitLookup.

Or, if not, you may be able to add that missing functionality to it yourself :-)

2. I really don't know off-hand. Please find out and let us know :-)

Response: I'm on fire today!

Seems like you've (or your blog) led me to the Holy Grail (or Pandora's Box):

The AdWindow.dll!

This contains a lot of extremely useful stuff for exploring the UI further; I can't believe I didn't look in a little deeper into this before.

I managed to find all the things I needed to get my AddinCommandBinding: Ids for all the user added external commands, what tab they are on and so on. However the dead-end was when I found that I could not create an AddinCommandBinding for them (maybe I need to do it during start-up? But then again why does the method exist in the UIApplication class?)

Anyway, so I went digging further inside the AdWindows.dll and found that the RibbonButton class has two useful events MouseEnter and MouseLeave. I could then store what button had the mouse over it and use that in combination with a mouse click event to capture what button was clicked. So I just plainly googled for click, but then started looking for click events inside the AdWindow. Couldn't find any useful events so I tried to think of how the events are named in the API.

'Execute' is the word you guys use for a lot of events. So I searched AdWindow for 'Execute' and found this event:

Autodesk.Windows.ComponentManager.ItemExecuted

Which is just spot on what I wanted – it fires every time a ribbon item is executed.

I just create a list of what buttons I want to monitor by iterating over the tabs using this:

  foreach(Autodesk.Windows.RibbonTab tab
    in Autodesk.Windows.ComponentManager.Ribbon.Tabs)
  {
    if (tab.KeyTip != null)
      continue;

    foreach(var panel in tab.Panels)
    {
      foreach(var item in panel.Source.Items)
      {

The KeyTip property is my way (didn't find any other) to figure out if the tab is user-created or not.

Problem solved!

I'm gonna test this for a while to see if performs well.

Have a nice day!

Answer: Thank you very much for your research and enthusiasm.

Rudolf Honke has published lots of interesting results making use of the not-officially-supported AdWindow.dll in conjunction with the officially supported Revit API functionality. They are grouped under The Building Coder category Automation.

You might also simply want to take a look at the results of searching for 'Rudolf Honke' on the blog.

I implemented a new external command named CmdItemExecuted in The Building Coder samples version 2015.0.118.0 to capture the gist of this discussion:

#region Namespaces
using System;
using System.Diagnostics;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Autodesk.Windows;
#endregion // Namespaces
 
[Transaction( TransactionMode.ReadOnly )]
class CmdItemExecuted : IExternalCommand
{
  static bool _subscribed = false;
 
  static void OnItemExecuted(
    object sender,
    Autodesk.Internal.Windows
      .RibbonItemExecutedEventArgs e )
  {
    string s = ( null == sender )
      ? "<nul>"
      : sender.ToString();
 
    Autodesk.Windows.RibbonItem parent = e.Parent;
 
    Debug.Print(
      "OnItemExecuted: {0} '{1}' in '{2}' cookie {3}",
      s, parent.AutomationName,
      e.Item.AutomationName, e.Item.Cookie );
  }
 
  public Result Execute(
    ExternalCommandData commandData,
    ref string message,
    ElementSet elements )
  {
    if( _subscribed )
    {
      Autodesk.Windows.ComponentManager.ItemExecuted
        -= OnItemExecuted;
 
      _subscribed = false;
    }
    else
    {
      RibbonTabCollection tabs
        = ComponentManager.Ribbon.Tabs;
 
      foreach( RibbonTab tab in tabs )
      {
        Debug.Print( "  {0} {1} '{2}'", tab,
          tab.GetType().Name, tab.AutomationName );
 
        if( tab.KeyTip == null )
        {
          // This tab is user defined.
 
          foreach( var panel in tab.Panels )
          {
            // Cannot convert type 'Autodesk.Windows.RibbonPanel' 
            // to 'Autodesk.Revit.UI.RibbonPanel' via a reference 
            // conversion, boxing conversion, unboxing conversion, 
            // wrapping conversion, or null type conversion.
            //
            //Autodesk.Revit.UI.RibbonPanel rp 
            //  = panel as Autodesk.Revit.UI.RibbonPanel;
 
            Autodesk.Windows.RibbonPanel rp
              = panel as Autodesk.Windows.RibbonPanel;
 
            Debug.Print( "    {0} {1}",
              panel.ToString(), panel.GetType().Name );
 
            foreach( var item in panel.Source.Items )
            {
              Autodesk.Windows.RibbonItem ri = item
                as Autodesk.Windows.RibbonItem;
 
              string automationName = ri.AutomationName;
 
              Debug.Print( "      {0} {1} '{2}' {3}",
                item.ToString(), item.GetType().Name,
                automationName, ri.Cookie );
            }
          }
        }
      }
 
      Autodesk.Windows.ComponentManager.ItemExecuted
        += OnItemExecuted;
 
      _subscribed = true;
    }
    return Result.Succeeded;
  }
}

Here is a sample log from the Visual Studio debug output console after running this code – copy and paste to an editor to see the truncated lines in full:

  UIFramework.RvtRibbonTab RvtRibbonTab 'Architecture'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Structure'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Systems'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Insert'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Annotate'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Analyze'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Massing & Site'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Collaborate'
  UIFramework.RvtRibbonTab RvtRibbonTab 'View'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Manage'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Create'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Insert'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Annotate'
  UIFramework.RvtRibbonTab RvtRibbonTab 'View'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Manage'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Add-Ins'
  Autodesk.Windows.RibbonTab RibbonTab 'Ribbon Sampler'
    Autodesk.Windows.RibbonPanel RibbonPanel
      Autodesk.Windows.RibbonButton RibbonButton 'Hello World' Item=CustomCtrl_%CustomCtrl_%Ribbon Sampler%Ribbon Sampler%PushButtonHello_Hello World_
      Autodesk.Windows.RibbonSplitButton RibbonSplitButton 'Command Data' Item=CustomCtrl_%CustomCtrl_%Ribbon Sampler%Ribbon Sampler%SplitButton_Split Button_
      Autodesk.Windows.RibbonSplitButton RibbonSplitButton 'Pulldown' Item=CustomCtrl_%CustomCtrl_%Ribbon Sampler%Ribbon Sampler%PulldownButton_Pulldown_
      UIFramework.RvtRibbonCombo RvtRibbonCombo '' Item=CustomCtrl_%CustomCtrl_%Ribbon Sampler%Ribbon Sampler%ComboBox__
      Autodesk.Windows.RibbonRowPanel RibbonRowPanel '' Item=__
      Autodesk.Windows.RibbonPanelBreak RibbonPanelBreak '' Item=__
      Autodesk.Windows.RibbonRadioButtonGroup RibbonRadioButtonGroup 'Command
 Data' Item=CustomCtrl_%CustomCtrl_%Ribbon Sampler%Ribbon Sampler%RadioButton__
      Autodesk.Windows.RibbonTextBox RibbonTextBox '' Item=CustomCtrl_%CustomCtrl_%Ribbon Sampler%Ribbon Sampler%Text Box__
  UIFramework.RvtRibbonTab RvtRibbonTab 'Modify'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Modify'
  UIFramework.RvtRibbonTab RvtRibbonTab 'In-Place Model'
  UIFramework.RvtRibbonTab RvtRibbonTab 'In-Place Mass'
  UIFramework.RvtRibbonTab RvtRibbonTab 'Family Editor'
OnItemExecuted: 'ADN Bc A-I' in 'Item Executed and List Command Buttons' cookie Item=CustomCtrl_%CustomCtrl_%CustomCtrl_%Add-Ins%RvtSamples%ADN Bc A-I%Item Executed and List Command Buttons_Item Executed and List Command Buttons_
OnItemExecuted: 'Basics' in 'About Revit' cookie Item=CustomCtrl_%CustomCtrl_%CustomCtrl_%Add-Ins%RvtSamples%Basics%About Revit_About Revit_
OnItemExecuted: 'Basics' in 'Hello Revit (CS)' cookie Item=CustomCtrl_%CustomCtrl_%CustomCtrl_%Add-Ins%RvtSamples%Basics%Hello Revit (CS)_Hello Revit (CS)_
OnItemExecuted: 'Basics' in 'Hello Revit (VB)' cookie Item=CustomCtrl_%CustomCtrl_%CustomCtrl_%Add-Ins%RvtSamples%Basics%Hello Revit (VB)_Hello Revit (VB)_