Cancel IExternalCommand?

DanielKP2Z9V
Advocate
Advocate

Cancel IExternalCommand?

DanielKP2Z9V
Advocate
Advocate

Is it possible to let user cancel an IExternalCommand for example by pressin Escape key? Command below lists all types in the current model, however in some models it can freeze revit for a few minutes. If that happens user should be able to cancel the operation.

 

using Autodesk.Revit.UI;
using Autodesk.Revit.DB;
using Autodesk.Revit.Attributes;
using System.Collections.Generic;
using System.Linq;

[Transaction(TransactionMode.Manual)]
public class ListTypesInCurrentModel : IExternalCommand
{
    public Result Execute(
        ExternalCommandData commandData,
        ref string message,
        ElementSet elements)
    {
        UIDocument uidoc = commandData.Application.ActiveUIDocument;
        Document doc = uidoc.Document;

        // Step 1: Collect all families in the document
        var allFamilies = new FilteredElementCollector(doc)
            .OfClass(typeof(Family))
            .Cast<Family>()
            .ToList();

        // Step 2: Prepare a list of type entries with family, type, and category information
        List<Dictionary<string, object>> typeEntries = new List<Dictionary<string, object>>();

        foreach (Family family in allFamilies)
        {
            // Get all types associated with the family
            var familySymbols = new FilteredElementCollector(doc)
                .OfClass(typeof(FamilySymbol))
                .OfCategoryId(family.FamilyCategoryId)
                .Cast<FamilySymbol>()
                .Where(symbol => symbol.FamilyName == family.Name) // Match by family name
                .ToList();

            foreach (var familySymbol in familySymbols)
            {
                var entry = new Dictionary<string, object>
                {
                    { "Type Name", familySymbol.Name },
                    { "Family", family.Name },
                    { "Category", familySymbol.Category.Name },
                    { "Type Element", familySymbol } // Store the element type for selection
                };

                typeEntries.Add(entry);
            }
        }

        // Step 3: Display the list of types using the CustomGUIs.DataGrid method
        var propertyNames = new List<string> { "Type Name", "Family", "Category" }; // Display only relevant columns
        var selectedEntries = CustomGUIs.DataGrid(typeEntries, propertyNames, spanAllScreens: false);

        if (selectedEntries.Count == 0)
        {
            return Result.Cancelled; // No selection made
        }

        // Step 4: Collect ElementIds of the selected types
        List<ElementId> selectedTypeIds = selectedEntries
            .Select(entry => (entry["Type Element"] as Element).Id)
            .ToList();

        // Step 5: Collect all instances of the selected types in the model
        var selectedInstances = new FilteredElementCollector(doc)
            .WherePasses(new ElementMulticlassFilter(new List<System.Type> { typeof(FamilyInstance), typeof(ElementType) }))
            .Where(x => selectedTypeIds.Contains(x.GetTypeId()))
            .Select(x => x.Id)
            .ToList();

        // Step 6: Select the instances in the model
        uidoc.Selection.SetElementIds(selectedInstances);

        return Result.Succeeded;
    }
}

 

0 Likes
Reply
248 Views
4 Replies
Replies (4)

jeremy_tammik
Autodesk
Autodesk

You can cancel an external command at any time simply by returning from the Execute method.

   

The return value is defined by the Result returned:

   

  

Failed The external application was unable to complete its task.
Succeeded The external application completed successfully. Autodesk Revit will keep this object during the entire Revit session.
Cancelled Signifies that the external application is cancelled.

  

However, the return value is irrelevant to your question. 

   

You can determine whether the Escape key is pressed by using the .NET libraries to query the keyboard status:

   

  

In your case, you could insert regular queries for Escape key being pressed inside your time-consuming loop.

  

However, I would also always very strongly recommend separating API interaction with the Revit database from UI operations such as pressing the Escape key. Just filtering for elements should not take a large amount of time. You can probably optimise that. That is a different question, however.

  

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

jeremy_tammik
Autodesk
Autodesk

Looking at the efficiency aspect of your code, a more efficient approach might be to just simply filter once only for all family instances. Looping over the instances, you can determine what family they belong to and other properties from the instance. That is probably more efficient than first filtering for families and then creating a new separate filter for the instances within each one of them.

   

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

DanielKP2Z9V
Advocate
Advocate

Thank you Jeremy, you are right that this code could have been much better if that filtering was done before for loop. After changing this bit, it now runs much faster to the extent where my original question about letting user to cancel it is no longer needed. However, surely it will come in handy on some other occasion and I will try your suggestions and report back then.

 

It actually runs so much faster now that I could do away with initial category selection and just list all family types in the project straight away.


changed this code:

 

 

using Autodesk.Revit.UI;
using Autodesk.Revit.DB;
using Autodesk.Revit.Attributes;
using System.Collections.Generic;
using System.Linq;

[Transaction(TransactionMode.Manual)]
public class ListTypesInCurrentModel : IExternalCommand
{
    public Result Execute(
        ExternalCommandData commandData,
        ref string message,
        ElementSet elements)
    {
        UIDocument uidoc = commandData.Application.ActiveUIDocument;
        Document doc = uidoc.Document;

        // Step 1: Collect all families in the document
        var allFamilies = new FilteredElementCollector(doc)
            .OfClass(typeof(Family))
            .Cast<Family>()
            .ToList();

        // Step 2: Prepare a list of type entries with family, type, and category information
        List<Dictionary<string, object>> typeEntries = new List<Dictionary<string, object>>();

        foreach (Family family in allFamilies)
        {
            // Get all types associated with the family
            var familySymbols = new FilteredElementCollector(doc)
                .OfClass(typeof(FamilySymbol))
                .OfCategoryId(family.FamilyCategoryId)
                .Cast<FamilySymbol>()
                .Where(symbol => symbol.FamilyName == family.Name) // Match by family name
                .ToList();

            foreach (var familySymbol in familySymbols)
            {
                var entry = new Dictionary<string, object>
                {
                    { "Type Name", familySymbol.Name },
                    { "Family", family.Name },
                    { "Category", familySymbol.Category.Name },
                    { "Type Element", familySymbol } // Store the element type for selection
                };

                typeEntries.Add(entry);
            }
        }

 

 

 

into this:

 

 

        // Step 7: Prepare a list of types within the selected category in one go
        List<Dictionary<string, object>> typeEntries = new List<Dictionary<string, object>>();
        Dictionary<string, FamilySymbol> typeElementMap = new Dictionary<string, FamilySymbol>(); // Map unique keys to FamilySymbols

        // Collect all FamilySymbol elements in the selected category
        var familySymbolsInCategory = new FilteredElementCollector(doc)
            .OfClass(typeof(FamilySymbol))
            .Where(symbol => symbol.Category.Id == selectedCategoryId) // Filter by the selected category
            .Cast<FamilySymbol>()
            .ToList();

        // Iterate through the FamilySymbols, instead of iterating through families
        foreach (var familySymbol in familySymbolsInCategory)
        {
            // Get the family for the current symbol
            Family family = familySymbol.Family;

            var entry = new Dictionary<string, object>
            {
                { "Type Name", familySymbol.Name },
                { "Family", family.Name },
                { "Category", familySymbol.Category.Name }
            };

            // Store the FamilySymbol with a unique key (Family:Type)
            string uniqueKey = $"{family.Name}:{familySymbol.Name}";
            typeElementMap[uniqueKey] = familySymbol;

            typeEntries.Add(entry);
        }

 

 


full code

0 Likes

rhanzlick
Advocate
Advocate

As @jeremy_tammik mentioned, the only way to “cancel” is to return the corresponding Result enum.

Due to Revit API’s single-threaded nature, there is no (straightforward) way to asynchronously detect an escape key-press and cancel. Any other options will likely be hacky, unreliable, or clunky at best.

 

(e.g. try cancelling the “open a Revit document” process mid-way, while it is technically possible the user experience is certainly not stellar)

0 Likes