RevitAppExtension is a template for creating Revit add-ins that integrate with Assistant. This template provides a foundation for building MVVM-based Revit applications with a modern UI using WPF-UI.
Create a new Extension by selecting the template "Revit App Extension for Assistant"

The template is organized according to the MVVM (Model-View-ViewModel) pattern:
The application starts with RevitAppExtensionCommand.cs, which implements the IRevitExtension<T> interface. The Run method is the entry point:
public IExtensionResult Run(IRevitExtensionContext context, RevitAppExtensionArgs args, CancellationToken cancellationToken)
{
// Check if a document is open
var document = context.UIApplication.ActiveUIDocument?.Document;
if (document is null)
return Result.Text.Failed("Revit has no active model open");
// Create service provider and register dependencies
var provider = ServiceFactory.Create(context.UIApplication, services =>
{
services.RegisterAppServices(args);
});
// Show the main window
WindowHandler.ShowWindow<MainWindow>(provider, context.UIApplication.MainWindowHandle);
return Result.Text.Succeeded("Application was started");
}
Initial user inputs are handled through the RevitAppExtensionArgs class:
public class RevitAppExtensionArgs
{
public string? InitialComment { get; set; }
// Add additional properties here as needed
}
This class can be extended with additional properties to capture different types of user input required for your application. Properties in this class are automatically parsed into UI elements in the Extension Task configuration in Assistant.
Dependencies are registered in the Registrations.cs file using the .NET Dependency Injection framework. The template uses a RegisterAppServices extension method on IServiceCollection to configure all required services:
public static IServiceCollection RegisterAppServices(this IServiceCollection services, RevitAppExtensionArgs args)
{
// Register user arguments
services.AddSingleton(args);
// Register CQRS handlers from the assembly
services.AddCqrs(typeof(Registrations).Assembly);
// Register UI components and view models
services.AddSingleton<MainWindow>();
services.AddSingleton<HomeViewModel>();
services.AddSingleton<AboutViewModel>();
services.AddSingleton<IContentDialogService, ContentDialogService>();
// Add additional services here
return services;
}
When adding new view models or services:
RegisterAppServices methodservices.AddSingleton<IMyService, MyService>()Views and ViewModels are connected through data templates in ViewBindings.xaml:
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewModels:HomeViewModel}">
<views:HomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:AboutViewModel}">
<views:AboutView />
</DataTemplate>
</ResourceDictionary>
This approach enables:
When adding a new view and view model:
The template follows a specific pattern for view models:
All view models inherit from RevitViewModelBase which provides common functionality:
The template supports two different command patterns depending on whether the command needs Revit context or not:
Send<TQuery, TResult>() patternDo(Method) patternpublic class HomeViewModel : RevitViewModelBase
{
// Injected services
private readonly IContentDialogService _contentDialogService;
private readonly ISnackbarService _snackbarService;
// Properties with notification
public string? CurrentDocumentTitle
{
get => Get<string?>();
set => Set(value);
}
public string? Comment
{
get => Get<string?>();
set => When(value)
.Notify(SetCommentOnSelectedElementsCommand)
.Set();
}
// Constructor with dependency injection
public HomeViewModel(
ViewModelBaseDeps dependencies,
RevitAppExtensionArgs args,
IContentDialogService contentDialogService,
ISnackbarService snackbarService
) : base(dependencies)
{
Comment = args.InitialComment;
_contentDialogService = contentDialogService;
_snackbarService = snackbarService;
}
// Commands using Send<> pattern for Revit-dependent operations
public IFluentCommand GetCurrentDocumentCommand =>
Send<GetDocumentTitleQuery, GetDocumentTitleQueryResult>()
.Then(o => CurrentDocumentTitle = o.Title);
public IFluentCommand SetCommentOnSelectedElementsCommand =>
Send<SetCommentOnSelectedElementsQuery, SetCommentOnSelectedElementsQueryResult>(() => new(Comment))
.If(() => !string.IsNullOrEmpty(Comment))
.Handle(OnSetCommentsFailed)
.Then(o => _snackbarService.Show("Comments set", o.Message, Wpf.Ui.Controls.ControlAppearance.Primary));
}
public class AboutViewModel(ViewModelBaseDeps dependencies, ISnackbarService snackbarService) : RevitViewModelBase(dependencies)
{
// Simple command using Do() pattern for non-Revit operations
public IFluentCommand ShowWikiCommand => Do(OpenWiki);
private void OpenWiki()
{
var wikiUrl = "https://wiki.cowitools.com"; // Update to your actual wiki domain
var startInfo = new ProcessStartInfo
{
FileName = wikiUrl,
UseShellExecute = true,
Verb = "open"
};
try
{
Process.Start(startInfo);
}
catch (Exception ex)
{
// Handle exception
snackbarService.Show("Error", $"Failed to open wiki: {ex.Message}", Wpf.Ui.Controls.ControlAppearance.Danger);
}
}
}
The template provides a robust property system that supports change notification and command interactions:
Get<T>() - Retrieves a property value from the view model's internal storageSet(value) - Sets a property value and raises change notificationWhen(value) - Begins a fluent API chain for more complex property updatesNotify(command) - Notifies a command that it should re-evaluate its executable stateSet() - Completes the property setting operation (used at end of a chain)// Simple property with basic change notification
public string? CurrentDocumentTitle
{
get => Get<string?>();
set => Set(value);
}
// Advanced property with command notification
public string? Comment
{
get => Get<string?>();
set => When(value) // Start a property update chain with the new value
.Notify(SetCommentOnSelectedElementsCommand) // Notify the command when this property changes
.Set(); // Complete the property update operation
}
The Notify() method is particularly important as it informs commands that a property they depend on has changed. When the Comment property changes:
When(value) method starts a chain for the new valueNotify(SetCommentOnSelectedElementsCommand) tells the command to re-evaluate its executable state
.If(() => !string.IsNullOrEmpty(Comment)).Set() method completes the property update and triggers UI refreshThis property system creates a reactive UI where:
Views are created using WPF and the WPF-UI library for modern UI components.
<UserControl x:Class="RevitAppExtension.Views.HomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
d:DataContext="{d:DesignInstance Type=viewmodels:HomeViewModel}">
<Grid>
<!-- UI Components -->
<ui:TextBlock Text="{Binding CurrentDocumentTitle}"/>
<ui:Button Content="Get document title"
Command="{Binding GetCurrentDocumentCommand}"/>
<ui:TextBox Text="{Binding Comment, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<ui:Button Content="Set Comment"
Command="{Binding SetCommentOnSelectedElementsCommand}"/>
</Grid>
</UserControl>
Key aspects of views:
ui:Button, ui:TextBlock, etc.){Binding PropertyName}d:DataContextThis template utilizes the WPF UI framework, a modern UI library designed to create desktop applications with a fresh, consistent look and feel that follows the Windows 11 design language.
To learn more about the WPF UI framework and its capabilities:
The template provides a fluent API for creating commands through the IFluentCommand interface with extension methods that support method chaining:
Send<TQuery, TResult>() - Creates a command that will execute a query handler with Revit contextDo(Action method) - Creates a command that executes a simple action with no Revit context neededIf(Func<bool> condition) - Adds a condition that must be true for the command to executeHandle(Func<Exception, Task> handler) - Specifies how to handle exceptions if they occurThen(Action<TResult> onSuccess) - Specifies the action to take when the command completes successfullyCancelCommand - Automatically available on commands created with Send<>() to cancel long-running operationsHere's an example of how these extension methods can be chained together to create sophisticated command flows:
public IFluentCommand SetCommentOnSelectedElementsCommand =>
Send<SetCommentOnSelectedElementsQuery, SetCommentOnSelectedElementsQueryResult>(() => new(Comment))
.If(() => !string.IsNullOrEmpty(Comment)) // Only execute if condition is true
.Handle(OnSetCommentsFailed) // Error handling
.Then(o => ShowSuccessMessage(o.Message)); // Success callback
The template uses the Command Query Responsibility Segregation (CQRS) pattern:
GetDocumentTitleQuery)DeleteSelectedElementsCommand)// Query definition
public class GetDocumentTitleQuery : IQuery<GetDocumentTitleQueryResult>;
public record GetDocumentTitleQueryResult(string Title);
// Query handler
public class GetDocumentTitleQueryHandler : IQueryHandler<GetDocumentTitleQuery, GetDocumentTitleQueryResult>
{
public GetDocumentTitleQueryResult Execute(GetDocumentTitleQuery input, CancellationToken cancellationToken)
{
// Implementation
}
}
// Command definition
public class DeleteSelectedElementsCommand;
// Command handler
public class DeleteSelectedElementsCommandHandler : ICommandHandler<DeleteSelectedElementsCommand>
{
public void Execute(DeleteSelectedElementsCommand input, CancellationToken cancellationToken)
{
// Implementation
}
}
Separation of Concerns
Dependency Injection
Registrations.csMVVM Pattern
Error Handling
.Handle() method on commands to manage errorsRevitAppExtensionArgs if neededRegistrations.csViewBindings.xaml if neededUse Visual Studio Code's Run and Debug view to start or attach the debugger:
Launch In Design Mode
Launch In Design Mode configuration in .vscode/launch.json.-c Design argument (bin/Design/RevitAppExtension.exe).IDesignQueryHandler<TQuery, TResult> implementations in the CQRS folder to provide mock data.GetDocumentTitleDesignQueryHandler or SetCommentOnSelectedElementsDesignQueryHandler.Attach to Revit
Attach to Revit configuration in .vscode/launch.json.Implement design-time handlers by creating classes that implement IDesignQueryHandler<TQuery, TResult>:
internal class MyQueryDesignHandler : IDesignQueryHandler<MyQuery, MyQueryResult>
{
public Task<MyQueryResult> HandleAsync(MyQuery request, CancellationToken cancellationToken)
{
// Return mock data for design mode
return Task.FromResult(new MyQueryResult(/* mock values */));
}
}
These handlers are automatically discovered and used when launching in Design Mode.
The template includes configuration for multiple Revit versions. Use the appropriate configuration for your target Revit version:
The Azure Pipelines configuration builds and packages the extension for all configured Revit versions. This is defined in azure-pipelines.yml
RevitAppExtension is only a template that you should change at your wish to make it fit your needs. In this section we will take a look at what features is implemented for demonstation purposes from a user prespective.
The application allows users to retrieve and display information about the current Revit document. The home screen shows the active document's title and provides a button to refresh this information.
Users can apply comments to selected elements in Revit:
The application provides visual feedback through notifications when comments are successfully applied, and includes error handling for failed operations.
The application offers a way to delete selected elements from the Revit model.
The application uses a navigation system with:
For long-running operations the application provides:
The application can receive initial input at startup, in this exampe a pre-populated comment field is available.