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;
}
}
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.
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.
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);
}
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)
Can't find what you're looking for? Ask the community or share your knowledge.