Recording Executed Commands

Recording Executed Commands

Anonymous
Not applicable
6,251 Views
20 Replies
Message 1 of 21

Recording Executed Commands

Anonymous
Not applicable

I am trying to write a script that records the commands that a user executes. Basically, I want to record the time and the command Id in a text file. I have written an External Application and used DocumentChanged Events. But, these events only report whether elements are modified and I don't know how to figure out what command has been executed that resulted in the modification. I greatly appreciate it if you could help me with this issue. 

Accepted solutions (2)
6,252 Views
20 Replies
Replies (20)
Message 2 of 21

matthew_taylor
Advisor
Advisor

Hi @Anonymous,

If you want to record user interface (UI) actions, you really need to deal with the Revit.UI namespace.

The user interface and the database (DB) actions are quite clearly defined, and separate.

To record what the user is clicking/executing you should look at the Revit.UI.Events namespace. There is definitely something suitable there.

 

Take a look. I think ExecutedEventArgs is your key search item.

 

-Matt


Cheers,

-Matt
_______________________________________________________________________________
Marking a post as a 'solution' helps the community. Giving a post 'Kudos' is as good as saying thanks. Why not do both?
Message 3 of 21

FAIR59
Advisor
Advisor
Accepted solution

you have to subscribe to this event:

Autodesk.Windows.ComponentManager.ItemExecuted

Message 4 of 21

matthew_taylor
Advisor
Advisor

@FAIR59 @Anonymous

Now that I have my code in front of me, I used Autodesk.Windows.ComponentManager.UIElementActivated.

I guess ItemExecuted probably works just as well!

 

-Matt


Cheers,

-Matt
_______________________________________________________________________________
Marking a post as a 'solution' helps the community. Giving a post 'Kudos' is as good as saying thanks. Why not do both?
Message 5 of 21

Anonymous
Not applicable

Thanks for the help. I am new to Revit API, and I don't know how to register the event in the External Application. I tried using "application.ControlledApplication" to register the event in the "OnStartup" method. But, ControlledApplication does not have a ItemExecuted method. 

 

 

0 Likes
Message 6 of 21

Anonymous
Not applicable

You need to add the adwindows.dll as a reference to your project.
Is use this myself, but it is not supported by autodesk, as far as I know,

Message 7 of 21

matthew_taylor
Advisor
Advisor

@Anonymous

In VB.NET, you'd do it like this:

'in OnStartup 
AddHandler Autodesk.Windows.ComponentManager.UIElementActivated, AddressOf MyUiElementActivated

'in OnShutdown
RemoveHandler Autodesk.Windows.ComponentManager.UIElementActivated, AddressOf MyUiElementActivated

Private Sub MyUiElementActivated(ByVal sender As Object, ByVal e As Autodesk.Windows.UIElementActivatedEventArgs)
   'do your thing
End Sub

I would imagine @FAIR59's suggestion would work in a similar manner.

 

 

-Matt

 

p.s. and of course, add the reference as @Anonymous suggested. As @Anonymous mentioned it's not supported by Autodesk, but I use it for Revit 2011-2017 without issue.


Cheers,

-Matt
_______________________________________________________________________________
Marking a post as a 'solution' helps the community. Giving a post 'Kudos' is as good as saying thanks. Why not do both?
Message 8 of 21

Anonymous
Not applicable

Thank you for the great help. Just a quick follow up, do you know how I can access the document through an ItemExecuted event handler? In addition to recording the executed commands, I want to record the selected elements (the elements that the command was executed on). 

I use the sender as application and access documents in other event handlers, but it seems I can't do the same with RibbonItemExecuted event handler. 

 

 

0 Likes
Message 9 of 21

Anonymous
Not applicable

Since its not a supported workflow you cannot expect to get the RevitAPI types.
I have not tried this myself, but I would suggest registering an ExternalEvent and raising that in the ItemExecuted event handler.
That is a great way of getting back into normal Revit context.

 

This will wait for the next idle moment to raise the event, however if your user unselectes the elements before the event is fired, you wont get an accurate selection.

A more compex way, if you find that you cant reliably capture the selected elements using a ExternalEvent, is using the IUpdater interface and make the updater trigger on the specific things you need to watch (in conjunction with the ItemExecuted event), then you would catch what command was executed and then you would get a valid context (in the updater) as the transaction is commited and therefore you can be sure of getting the selected elements.
This will propably require some testing to cover all scenarios.

 

0 Likes
Message 10 of 21

matthew_taylor
Advisor
Advisor

@Anonymous I agree with @Anonymous

That said, you can access the db document if you use the ViewActivated event to store it (or something similar). Your UI event subroutine could then reference that. I can't see how you'd get to the uidocument (in order to get the current selection) though!

An iUpdater will give you the added/changed/deleted elements, which isn't technically the selection (as some of the selection may remain unchanged), but that may be as good as you get (or as good as you need).

 

How about you take a step back from the task and tell us exactly what you're trying to achieve so we can advise you better?

 

Cheers,

 

-Matt


Cheers,

-Matt
_______________________________________________________________________________
Marking a post as a 'solution' helps the community. Giving a post 'Kudos' is as good as saying thanks. Why not do both?
0 Likes
Message 11 of 21

Anonymous
Not applicable

@matthew_taylor, storing the document object and then accessing it from a non-revit-api context is not advisable. I stopped doing such things because there will be crashes.


I suggested the IUpdater because it will deliver the document being changed (from UpdaterData.GetDocument()) and from that you can construct your UIDocument and retrieve the selection.

 

But I agree with you @matthew_taylor:

@Anonymous: elaborate on what you want to accomplish and we can suggest something more powerful and simple, perhaps.

0 Likes
Message 12 of 21

Anonymous
Not applicable

Thank you for your response. I am trying to record all model development events, by which I mean the commands that the modeler executes and the corresponding changes to the elements in the model. I want to record the time, the type of command, element GUID, and the project that the modeler is working on. 

0 Likes
Message 13 of 21

matthew_taylor
Advisor
Advisor
Hi @Anonymous,
I wonder whether what you're trying to do should be achieved by an iupdater as Erik said. I've not looked into ActiveAddinId, but I wonder whether you could assemble a dictionary of AddinIds and their associated ribbon data (in text form?) at startup, and then reference that with your iupdater, as you look like you should be able to reference the ActiveAddinId from within the updater.
I've not worked with ActiveAddinId as it's new in 2017, but it may be what you're after.

-Matt

Cheers,

-Matt
_______________________________________________________________________________
Marking a post as a 'solution' helps the community. Giving a post 'Kudos' is as good as saying thanks. Why not do both?
Message 14 of 21

Anonymous
Not applicable
Accepted solution

@matthew_taylor, good tip on that ActiveAddinId, Im gonna take a look at that. It might come in handy to see what is actually executing when you're catching events.

However, looking at the documentation, that will only give you the external command or application Id.
What @Anonymous seems to want is to get all changes like the user creates a wall etc.

 

Then I suggest the following: register a IUpdater that triggers on the elements you want to record: I suggest inverted ElementIsElementTypeFilter to get all created instances and add ElementIsElementTypeFilter without inversion to get basically all element changes and trigger on addition, deletion, geometry and any (you'll know what I mean when you look at the documentation).

Also subscribe to the ItemExecuted event like we discussed earlier.
The IUpdater is going to fire first so in that event you cache all the info you want to store in persistable objects. No revit api types,  you can end up crashing revit if you ask for anything from any revit element out of context or you can get data that is out of date. Create new classes that hold the info in primitive types. When the ItemExecuted event fires you send the info to your database or logfile or what ever you are into.

Since you will be catching alot of events its a good idea to keep your implementation as lightweight as possible to minimize lag for the user.

Also when you send your data, send it in a separate Thread or Task, so that the handler returns fast, but like I wrote earlier, make sure you dont have any revit api types.

 

Thats about it, you might want to look at how the ItemExecuted behaves and filter the event accordingly because it fires quite alot. Almost everytime the user exists a command it goes to the Modify command (for instance).

 

Good luck!

Message 15 of 21

Anonymous
Not applicable

Thank you all for the great suggestions. I will try the recommended methods and report the outcome. 

0 Likes
Message 16 of 21

matthew_taylor
Advisor
Advisor
Hi Erik,
That was my intention, to reference the addin data from within the iupdater (that captures the changes). An iupdater would be overkill for *solely* tracking addin usage etc, as I'm sure you're aware. 😂

Cheers,

-Matt

Cheers,

-Matt
_______________________________________________________________________________
Marking a post as a 'solution' helps the community. Giving a post 'Kudos' is as good as saying thanks. Why not do both?
0 Likes
Message 17 of 21

n_mulconray
Advocate
Advocate

I have recently been asked to perform an analysis of custom add-in usage, to determine where our efforts should be applied as in additional training, effort to improve and or deprecation of add-ins that are not used. Has anyone had any success with this as I don't have much time to devote to developing a bespoke solution and would only use it infrequently. I would ideally get an add-ins visible name and pair that with a dateTime field and record it.

0 Likes
Message 18 of 21

anton.shaw
Explorer
Explorer

Hi Nik

 

Did you develop this idea any further?

 

I'm just putting some research into something now to do a very similar thing around add-in use.

 

Thanks,

Anton

0 Likes
Message 19 of 21

n_mulconray
Advocate
Advocate

Hey Anton are you a Kiwi? Are you friends with the Kiwi codes developers, I used to work at Meteir 3 in Melbourne with one of them. Anyway I used the following code below. This is not ideal and was just meant as a temporary solution as the output is to an delimited file that i visualize in powerbi. Ideally you would send to a database and could then leverage the data elsewhere.

 

 public void OnDocClosing(object sender, DocumentClosingEventArgs e)
{
try
{
//saves to a tab delimiter to .csv
RecordAddinData.WhatExternalCommandWasExecuted(e.Document, MySQLDateTimeConverter(DateTime.Now)); 

}

 

using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Analysis;
using System;
using System.Collections.Generic;
using System.IO;

namespace TechnoCore
{
internal static class RecordAddinData
{
internal static List<string> WhatExternalCommandWasExecuted(Document doc, DateTime dateTimeClosing)
{

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

try
{
string userName = GetThreeInitials(doc);
bool isFamilyDoc = doc.IsFamilyDocument;
string projType;
string projStatus;


if (isFamilyDoc)
{
projType = "NULL";
projStatus = "NULL";
}
else
{
EnergyDataSettings energyDataSetting = EnergyDataSettings.GetFromDocument(doc);
projType = energyDataSetting.BuildingType.ToString();

ProjectInfo m_info = doc.ProjectInformation;
projStatus = !string.IsNullOrWhiteSpace(m_info.Status) ? m_info.Status : null;
}

 

string journalPath = doc.Application.RecordingJournalFilename;
List<string> JournalLines = new List<string>();
string searchString = "Jrn.RibbonEvent \"Execute external command:";

var fs = new FileStream(journalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using (var sr = new StreamReader(fs))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (line.Contains(searchString))
{
int pFrom = line.IndexOf(":PT_") + ":PT_".Length;
int pTo = line.LastIndexOf("\"");
string result = line.Substring(pFrom, pTo - pFrom);

if (line.ToUpperInvariant().Contains("DLM INTEGRATOR")) { result = "Delslack.Infiltrator";}
else if (line.ToUpperInvariant().Contains("UPREV")) { result = "Xrev.Uprev"; }
else if (line.ToUpperInvariant().Contains("REVIT LOOKUP")) { result = "Inspector.RevitLookup"; }

commandsExecuted.Add(
userName + "\t" +
result + "\t" +
projType + "\t" +
projStatus + "\t" +
dateTimeClosing + "\t" +
doc.IsFamilyDocument.ToString()
);
}
}
}

AppendTextLinesToNotepad(commandsExecuted, PATHS.ADDINS_SVR(doc, @"External-Command-Usage.csv"));
}
catch (Exception) { }

return commandsExecuted;
}

internal static string GetThreeInitials(Document _doc)
{
string userName = _doc.Application.Username.ToUpperInvariant().Replace(".", "");

switch (userName.Length)
{
case 0:
return "XYZ";
case 1:
return userName + "YZ";
case 2:
return userName + "Z";
default:
return userName.Substring(0, 3);
}
}

private static void AppendTextLinesToNotepad(List<string> list, string filename)
{
using (StreamWriter outputFile = new StreamWriter(filename, true))
{
foreach (string item in list)
{
outputFile.WriteLine(item);
}
}
}

}//cl
}//ns

0 Likes
Message 20 of 21

jeremy_tammik
Alumni
Alumni

You might be able to extract the required information quite easily by running a regular analysis on all the journal files. This possibility is mentioned among other approaches in the discussion on monitoring using the SLOG:

 

https://thebuildingcoder.typepad.com/blog/2021/12/logging-and-monitoring-deleted-data.html

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open