Innovative Revit Add-in Development (part 2)
In this second part of a three-part blog series, I'll guide you through incorporating Inversion of Control (IoC) into your Revit add-in development. We'll replace the built-in .NET IoC container with Autofac, the most widely used IoC container in the .NET space.
Innovative Revit Add-in Development (part 1)
Innovative Revit Add-in Development (part 3)
IoC-Containers
Inversion of Control (IoC) is a design principle that advocates the reversal of control. Instead of a class creating and managing its own dependencies, this task is delegated to a container. The container is responsible for creating, managing, and controlling the life-cycle of objects. The class itself no longer needs to worry about creating dependencies. For a deeper understanding, see Martin Fowler's articles Inversion Of Control and Inversion of Control Containers and the Dependency Injection pattern.
Benefits of Using IoC Containers
Using an IoC container can be beneficial in several scenarios:
Scalability and maintainability
As your application grows and becomes more complex, an IoC container can simplify dependency management. It improves scalability and maintainability by automating the creation and resolution of dependencies.Testability
IoC containers make testing easier, especially mocking dependencies. You can easily create test cases by providing artificial implementations for specific dependencies.Decoupling
An IoC container helps decouple dependencies between components. This makes code more flexible and easier to replace.Modularity
If you intend to divide your application into modules, an IoC container can streamline dependency management between those modules.Frameworks and libraries
Many frameworks and libraries use IoC containers to manage their own dependencies. If you're using a framework that supports an IoC container, it makes sense to use it in your application as well.
Keep in mind that using an IoC container can also introduce overhead. For smaller applications or simple dependencies, manual management may be easier. The decision depends on the specific requirements of your application.
Lifetime-Scopes
As mentioned earlier, IoC containers manage objects and their life cycles. Objects that are needed temporarily, such as during command execution, should be created within a lifetime scope. This scope is created when a command starts and closed when the command ends. All objects created within the lifetime scope are automatically destroyed when the command ends (scope closure) and the memory used is reclaimed.
In Revit, however, we need to think about scopes not only at the command level. When we load the Revit add-in, it's necessary to create the IoC container and thus the outer lifetime scope. We can use this to create objects such as updaters on Revit startup. When objects are registered as singletons, they can be accessed by multiple commands. In this case, be sure to only create objects where you really need the same instance in different commands. This can be useful if accessing and initializing a shared resource is expensive (e.g. time consuming).
Scotec.Revit
If you want to use IoC containers in your Revit add-in, you need to do some preliminary work. The NuGet package Scotec.Revit reduces this effort. It contains easy-to-use base implementations for Revit apps, commands, updaters, and WPF windows.
In the following sections, I'll explain some of the steps you need to take using code snippets. The full source code for this article can be found on GitHub in the scotec-revit-tutorial project.
Creating a Revit App
Erstellen Sie Ihre Revit-App mit IoC-Container einfach, indem Sie in Ihrem Projekt das Nuget-Paket Scotec.Revit referenzieren. Erstellen Sie eine neue App-Klasse und leiten Sie diese von RevitApp
ab.
public class RevitTutorialApp : RevitApp
{
}
Override the OnConfigure()
method to register your services with the service locator.
protected override void OnConfigure(IHostBuilder builder)
{
base.OnConfigure(builder);
// Register services by using Autofac modules.
builder.ConfigureContainer<ContainerBuilder>(containerBuilder =>
containerBuilder.RegisterModule<RegistrationModule>());
builder.ConfigureServices(services =>
{
// You can also register services here.
// However, using autofac modules gives you more flexibility.
});
}
Within the OnStartup()
method, you have the ability to run initialization code. For example, add tabs, panels, or buttons to the ribbon bar.
protected override Result OnStartup()
{
try
{
// var yourService = Services.GetService<YourService>();
// Create tabs, panels ond buttons
TabManager.CreateTab(Application, StringResources.Tab_Name);
var panel = TabManager.GetPanel(Application, StringResources.Panel_Name, StringResources.Tab_Name);
var button = (PushButton)panel.AddItem(CreateButtonData("Revit.Tutorial",
StringResources.Command_Test_Text, StringResources.Command_Test_Description,
typeof(TestCommand)));
button.Enabled = true;
}
catch (Exception)
{
return Result.Failed;
}
return Result.Succeeded;
}
Use the OnShutdown()
method to release resources requested during startup.
protected override Result OnShutdown()
{
return Result.Succeeded;
}
When the add-in is loaded, the Revit App is automatically instantiated. It registers the following objects with the IoC container:
Autodesk.Revit.UI.UIControlledApplication
Represents the Autodesk Revit user interface.Autodesk.Revit.DB.AddInId
ID of the current Revit add-in.Autodesk.Revit.ApplicationServices.ControlledApplication
Represents the Autodesk Revit application.
You can access these objects from all add-in commands.
The Revit application is registered as usual using an .addin manifest and automatically instantiated by Revit when the add-in is loaded.
<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
<AddIn Type="Application">
<Name>Revit Tutorial</Name>
<FullClassName>Revit.Tutorial.RevitTutorialApp</FullClassName>
<Assembly>.\Revit.Tutorial\Revit.Tutorial.dll</Assembly>
<AddInId>C58F0D46-D4E2-44FC-A0E1-48C5904AF877</AddInId>
<VendorId>SCOTEC</VendorId>
<VendorDescription>scotec Software Solutions AB</VendorDescription>
</AddIn>
</RevitAddIns>
Creating Commands
Auch Kommandos lassen sich auf einfache Weise erstellen. Leiten Sie dazau eine neue Klasse von RevitCommand
ab und überschreiben Sie die OnExecute()
Methode.
Commands are also easy to create. Derive a new class from RevitCommand
and override the OnExecute()
method.
[Transaction(TransactionMode.Manual)]
public class TestCommand : RevitCommand
{
public TestCommand()
{
NoTransaction = true;
}
protected override string CommandName => StringResources.Command_Test_Name;
protected override Result OnExecute(ExternalCommandData commandData, IServiceProvider services)
{
ShowTestWindow(services);
return Result.Succeeded;
}
private static void ShowTestWindow(IServiceProvider services)
{
var window = services.GetService<TestWindow>();
window.ShowDialog();
}
}
To execute the command, Revit calls the Execute()
method. Within this method, a new lifetime scope is first created. This ensures that any objects created by the IoC container during command execution will be released after the command has finished.
In most cases, you'll need to access the document, the active view, and the UIApplication while the command is running. Therefore, all three objects are registered with the IoC container and remain available throughout the command execution.
The next step is to start a transaction and call the overloaded OnExecute()
method. Depending on the return value, the transaction is either committed or rolled back.
Transactions in Revit provide the ability to handle errors that may occur before Revit processes them. This is done by registering error handlers with the transaction. This allows you to evaluate and potentially handle errors within your command class. Simply override the OnPreprocessFailures()
and OnProcessFailures()
methods in the derived class.
In certain scenarios, you may not want to initiate a transaction while the command is running. For example, the command may not modify the current document, or you may want to manage the transaction yourself. In such cases, set the NoTransaction
property to true
.
NoTransaction = true;
Note: If a command is executed when no document is open, no transaction is initiated regardless of the status of the NoTransaction
property.
Updaters and Command Availability
Scotec.Revit
enthält neben den Basisimplementierungen für die Apps und Kommandos ebenfalls Klassen, welche das IUpdater
Interface sowie das IExternalCommandAvailability
Interface implementieren.
Verwenden Sie diese Klassen, wenn Sie Updater benötigen oder wenn Sie den Status der Buttons im Ribbon setzen möchten. Selbstverständlich steht Ihnen auch hier der IoC-Container zur Verfügung.
In addition to the base implementations for applications and commands, Scotec.Revit
also includes classes that implement the IUpdater
interface and the IExternalCommandAvailability
interface.
Use these classes if you need updaters or if you want to set the status of the buttons in the ribbon. Of course, the IoC container is also available here.
Autofac IoC-Container
.NET enthält einen eigenen IoC-Container, welcher bereits einen großen Teil der benötigten Funktionalität abdeckt. Scotec.Revit
ersetzt diesen jedoch durch Autofac. Autofac bietet zahlreiche zusätzliche Features wie z.B. Named und Keyed Services, Delegate Factories oder Pooling. Scotec.Revit
nutzt unter anderem die Möglichkeit, beim Erstellen eines neuen Lifetime-Scopes neue Services zu registrieren, welche dann beim Schließen des Scopes automatisch wieder entfernt werden. Die zusätzlich registrierten Services stehen somit nur innerhalb dieses Scopes zur Verfügung.
.NET comes with its own built-in IoC container that already covers a significant part of the required functionality. However, Scotec.Revit
replaces this with Autofac. Autofac offers many additional features such as named and keyed services, delegate factories or pooling. Scotec.Revit
uses the ability to register new services when creating a new lifetime scope. These services are then automatically removed when the scope is closed. Consequently, the additionally registered services are only accessible within that specific scope.
Need more information?
I'd be happy to help. If you have any questions about this part of the blog series or using Scotec.Revit
, please don't hesitate to contact me.
Need help developing your Revit add-in? Then let's schedule a call or face-to-face meeting to discuss the details.
Innovative Revit Add-in Development (part 1)
Innovative Revit Add-in Development (part 3)