Detail Blog
Nhu.Truong
Developing a Dynamo package using DI pattern
Developing a Dynamo package to accurately position Soffit Corner elements using Dynamo API, Revit API, and C# with the Dependency Injection pattern.
Requirement
Use the Soffit Corner families to place them in the correct positions as illustrated in the image below.
Why should we use the Dependency Injection pattern?
Advantages | Disadvantages |
Makes writing unit tests easier. | Complex implementation. |
Minimize boilerplate code. | Some compile-time errors are pushed to runtime. |
Enhances scalability and reusability. | Affecting the auto-complete or find references functionality of some IDEs. |
Creates loose coupling, reduces tight dependencies. | Risk of potential dependency loops. |
There are several different styles of dependency injection: Interface injection, constructor injection, setter injection, and method injection. In this post, constructor injection is used.
Tools
- Autodesk Revit 2024
- Dynamo 2.19
- Visual Studio 2022
References
- DynamoCore.dll, DynamoCoreWpf.dll, DynamoServices.dll, ProtoGeometry.dll.
- RevitAPI.dll, RevitAPIUI.dll, RevitNodes.dll, RevitServices.dll.
- Microsoft.Extensions.DependencyInjection.dll.
Programming Language
- C# 9.0 (enable nullable), .NET Framework 4.8.
Project Structure
BeyConsNodes
|--- Components
|--- Formwork.cs
|--- Extensions
|--- SelectionExtension.cs
|--- SoffitCornerExtension.cs
|--- Factories
|--- IFamilySymbolFactory.cs
|--- ISoffitCornerFactory.cs
|--- FamilySymbolFactory.cs
|--- SoffitCornerFactory.cs
|--- Models
|--- FamilySymbolModel.cs
|--- SoffitCornerModel.cs
|--- Services
|--- IFamilySymbolService.cs
|--- IMessageService.cs
|--- ISoffitCornerService.cs
|--- FamilySymbolService.cs
|--- MessageService.cs
|--- SoffitCornerService.cs
|--- Settings
|--- ToleranceSetting.cs
|--- CornerType.cs
|--- Hosting.cs
|--- ViewExtension.cs
|--- pkg.json
|--- BeyConsNodes_DynamoCustomization.xml
|--- ViewExtension_ViewExtensionDefinition.xml
MSBuild Configuration
<PropertyGroup> <PostBuildEvent> echo F| xcopy /y /d "$(ProjectDir)$(OutDir)*.dll" "$(AppData)\Dynamo\Dynamo Revit\2.19\packages\BeyCons\bin\" echo F| xcopy /y /d "$(ProjectDir)pkg.json" "$(AppData)\Dynamo\Dynamo Revit\2.19\packages\BeyCons" echo F| xcopy /y /d "$(ProjectDir)ViewExtension_ViewExtensionDefinition.xml" "$(AppData)\Dynamo\Dynamo Revit\2.19\packages\BeyCons\extra\" echo F| xcopy /y /d "$(ProjectDir)BeyConsNodes_DynamoCustomization.xml" "$(AppData)\Dynamo\Dynamo Revit\2.19\packages\BeyCons\bin\" </PostBuildEvent> </PropertyGroup>
Configure the pkg.json file so that Dynamo can read the package.
{ "license": "MIT", "file_hash": null, "name": "BeyCons", "version": "1.0.1", "description": "Package for Dynamo", "group": "", "keywords": [ "beycons", "nodes" ], "dependencies": [], "host_dependencies": [ "Revit" ], "contents": "", "engine_version": "2.19.0.6156", "engine": "dynamo", "engine_metadata": "", "site_url": "https://beycons.net", "repository_url": "", "contains_binaries": true, "node_libraries": [ "BeyConsNodes, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null" ] }
Mapping namespaces to custom categories via the BeyConsNodes_DynamoCustomization.xml file.
<?xml version="1.0" encoding="utf-8" ?>
<doc>
<assembly>
<name>BeyConsNodes</name>
</assembly>
<namespaces>
<namespace name="BeyConsNodes.Components">
<category>BeyCons</category>
</namespace>
</namespaces>
</doc>
ViewExtension_ViewExtensionDefinition.xml manifest file informs Dynamo about the location of the Extension's dll.
<ViewExtensionDefinition> <AssemblyPath>..\bin\BeyConsNodes.dll</AssemblyPath> <TypeName>BeyConsNodes.ViewExtension</TypeName> </ViewExtensionDefinition>
The ViewExtension type implements Dynamo's IViewExtension.
#region Using using BeyConsNodes.Services; using Dynamo.Graph.Nodes; using Dynamo.Wpf.Extensions; #endregion #nullable enable namespace BeyConsNodes { internal class ViewExtension : IViewExtension { #region Field private IFamilySymbolService? _familySymbolService; private ViewLoadedParams? _viewLoadedParams; #endregion #region Implement public string UniqueId => "83F0340B-FE31-431A-BBD1-1D15C9D3CD4E"; public string Name => "BeyCons View Extension"; public void Dispose() { } public void Loaded(ViewLoadedParams viewLoadedParams) { Hosting.StartHosting(); _familySymbolService = Hosting.GetService<IFamilySymbolService>(); _viewLoadedParams = viewLoadedParams; _viewLoadedParams.CurrentWorkspaceModel.NodeAdded += NodeAdded; } public void Shutdown() { if (_viewLoadedParams is not null) _viewLoadedParams.CurrentWorkspaceModel.NodeAdded -= NodeAdded; Hosting.StopHosting(); } public void Startup(ViewStartupParams viewStartupParams) { } #endregion #region Implement Events private void NodeAdded(NodeModel nodeModel) { if (nodeModel.Name == $"{nameof(Components.Formwork)}.{nameof(Components.Formwork.CreateCorners)}" && _familySymbolService?.IsValidSoffitCornerType() is not true) nodeModel.IsFrozen = true; } #endregion } }
The Hosting class is utilized for injecting and resolving services.
#region Using
using BeyConsNodes.Extensions;
using Microsoft.Extensions.DependencyInjection;
using RevitServices.Persistence;
#endregion
#nullable enable
namespace BeyConsNodes
{
internal static class Hosting
{
#region Field
private static ServiceProvider? _serviceProvider;
#endregion
#region Method
public static void StartHosting()
{
var services = new ServiceCollection();
services.AddSoffitCorner(DocumentManager.Instance.CurrentDBDocument);
_serviceProvider = services.BuildServiceProvider();
}
public static void StopHosting()
{
_serviceProvider?.Dispose();
}
public static T? GetService<T>() where T : class
{
return _serviceProvider?.GetService<T>();
}
#endregion
}
}
The AddSoffitCorner is an extension method used for injecting various services into the ServiceCollection and is implemented in the SoffitCornerExtension class.
#region Using
using Autodesk.Revit.DB;
using BeyConsNodes.Factories;
using BeyConsNodes.Services;
using BeyConsNodes.Settings;
using Microsoft.Extensions.DependencyInjection;
#endregion
namespace BeyConsNodes.Extensions
{
internal static class SoffitCornerExtension
{
private static IServiceCollection AddToleranceSetting(this IServiceCollection serviceCollection)
{
var setting = new ToleranceSetting
{
Length = 1e-4,
Area = 1e-7
};
serviceCollection.AddSingleton(setting);
return serviceCollection;
}
public static IServiceCollection AddSoffitCorner(this IServiceCollection serviceCollection, Document document)
{
serviceCollection.AddToleranceSetting();
serviceCollection.AddSingleton(document);
serviceCollection.AddScoped<IFamilySymbolFactory, FamilySymbolFactory>();
serviceCollection.AddScoped<ISoffitCornerFactory, SoffitCornerFactory>();
serviceCollection.AddScoped<IMessageService, MessageService>();
serviceCollection.AddScoped<IFamilySymbolService, FamilySymbolService>();
serviceCollection.AddScoped<ISoffitCornerService, SoffitCornerService>();
return serviceCollection;
}
}
}
Custom nodes (CreateCorners) in Dynamo are implemented in the Formwork class.
#region Using using Autodesk.DesignScript.Runtime; using Autodesk.Revit.DB; using Autodesk.Revit.DB.Architecture; using BeyConsNodes.Extensions; using BeyConsNodes.Services; using Dynamo.Graph.Nodes; using RevitServices.Persistence; using RevitServices.Transactions; using System; using System.Collections.Generic; #endregion namespace BeyConsNodes.Components { public class Formwork { private Formwork() { } [NodeCategory("Create")] public static List<Revit.Elements.FamilyInstance> CreateCorners(Revit.Elements.Room room, Autodesk.DesignScript.Geometry.Surface slabFace) { var document = DocumentManager.Instance.CurrentDBDocument; if (slabFace.GetRevitFaceReference() is not { } reference || document.GetElement(reference)?.GetGeometryObjectFromReference(reference) is not PlanarFace planarFace) throw new ArgumentException($"{slabFace} is required the plane."); if (room.InternalElement is not Room revitRoom) throw new ArgumentException($"{room} is required the room element."); var cornerInstances = new List<Revit.Elements.FamilyInstance>(); if (Hosting.GetService<ISoffitCornerService>() is not { } soffitCornerService) return cornerInstances;TransactionManager.Instance.EnsureInTransaction(document);
var soffitCornerInstances = soffitCornerService.CreateSoffitCornerInstance(revitRoom, planarFace); foreach (var soffitCornerInstance in soffitCornerInstances) cornerInstances.Add((Revit.Elements.FamilyInstance)Revit.Elements.ElementWrapper.ToDSType(soffitCornerInstance, true)); TransactionManager.Instance.TransactionTaskDone(); return cornerInstances; } } }
An instance of the SoffitCornerService class is initialized through the injector.
var soffitCornerService = Hosting.GetService<ISoffitCornerService>();
#region Using using Autodesk.Revit.DB; using Autodesk.Revit.DB.Architecture; using System.Collections.Generic; #endregion namespace BeyConsNodes.Services { internal interface ISoffitCornerService { IEnumerable<FamilyInstance> CreateSoffitCornerInstance(Room room, PlanarFace planarFace); } }
#region Using using Autodesk.Revit.DB; using Autodesk.Revit.DB.Architecture; using Autodesk.Revit.DB.Structure; using BeyConsNodes.Factories; using BeyConsNodes.Models; using System.Collections.Generic; #endregion namespace BeyConsNodes.Services { internal class SoffitCornerService : ISoffitCornerService { #region Field private readonly ISoffitCornerFactory _soffitCornerFactory; private readonly IFamilySymbolService _familySymbolService; #endregion public SoffitCornerService(ISoffitCornerFactory soffitCornerFactory, IFamilySymbolService familySymbolService) { _soffitCornerFactory = soffitCornerFactory; _familySymbolService = familySymbolService; } #region Implement public IEnumerable<FamilyInstance> CreateSoffitCornerInstance(Room room, PlanarFace planarFace) { var soffitCornerInstances = new List<FamilyInstance>(); var document = room.Document; var soffitCornerModels = _soffitCornerFactory.CreateSoffitCornerModel(room); foreach (var soffitCornerModel in soffitCornerModels) { var soffitCornerType = _familySymbolService.GetSoffitCornerType(soffitCornerModel.CornerType); var soffitCornerInstance = document.Create.NewFamilyInstance(soffitCornerModel.PlacePoint, soffitCornerType, room.Level, StructuralType.NonStructural); RotateSoffitCornerInstance(soffitCornerInstance, soffitCornerModel); SetHeightSoffitCornerInstance(soffitCornerInstance, planarFace); soffitCornerInstances.Add(soffitCornerInstance); } return soffitCornerInstances; } #endregion #region Method private void SetHeightSoffitCornerInstance(FamilyInstance soffitCornerInstance, PlanarFace planarFace) { if (soffitCornerInstance.get_Parameter(BuiltInParameter.INSTANCE_SCHEDULE_ONLY_LEVEL_PARAM) is not { } levelParameter || levelParameter.AsElementId() is null) return; if (soffitCornerInstance.Document.GetElement(levelParameter.AsElementId()) is not Level level) return; if (soffitCornerInstance.get_Parameter(BuiltInParameter.INSTANCE_ELEVATION_PARAM) is not { } offsetParameter) return; var offsetHeight = planarFace.Origin.Z - level.Elevation; offsetParameter.Set(offsetHeight); } private void RotateSoffitCornerInstance(FamilyInstance soffitCornerInstance, SoffitCornerModel soffitCornerModel) { var lineAxis = Line.CreateBound(soffitCornerModel.PlacePoint, Transform.CreateTranslation(XYZ.BasisZ).OfPoint(soffitCornerModel.PlacePoint)); var rotateAngle = CalculateRotateAngle(soffitCornerModel.Bisector); ElementTransformUtils.RotateElement(soffitCornerInstance.Document, soffitCornerInstance.Id, lineAxis, rotateAngle); } private double CalculateRotateAngle(XYZ bisector) { var originBisector = new XYZ(1, 1, 0); var angle = originBisector.Normalize().AngleTo(bisector); if (bisector.Y < bisector.X) return -angle; return angle; } #endregion } }
When the injector initializes an instance of the SoffitCornerService class, it also initializes instances of the ISoffitCornerFactory and IFamilySymbolService classes and passes them into the constructor of the SoffitCornerService class. Similarly, when the instance of the SoffitCornerFactory is initialized, the injector will autonomously instantiate the necessary instances and pass them into the constructor of the SoffitCornerFactory class.
Result
Conclusion
- The post explains how to implement a Dynamo package using the C# language with the Dependency Injection pattern.
- Implementing with the Dependency Injection pattern aids in writing cleaner code, facilitates teamwork, eliminates the need to instantiate objects in multiple places, and, when well-organized, helps minimize the passing of numerous parameters to methods. It also reduces dependencies between objects through interfaces, making it easier to fake data while writing unit tests.