Detail Blog

Nhu.Truong
Nhu.Truong
8Blogs
3Followers
933Views
0Like
Created on 15/12/2023
Category - Dynamo API

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>
After building, the folder structure is as follows
$(AppData)\Dynamo\Dynamo Revit\2.19\packages
BeyCons
|--- bin
      |--- BeyConsNodes_DynamoCustomization.xml
      |--- BeyConsNodes.dll
      |--- . . . . .
|--- extra
      |--- ViewExtension_ViewExtensionDefinition.xml
|--- pkg.json

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.
If you have any questions or suggestions, please create a topic in this link.
C#
Dynamo
Modified on 23/04/2024

Related Blogs