Thursday, July 13, 2017

Accessing elements in Revit links with Dynamo

Out-of-the-box Dynamo cannot actually access to Revit elements that are inside a linked model. Dealing with linked models is a common practice when BIM managers check federated models. I believe that Dynamo is the best tool for model checking. Its flexibility lets you pretty much do anything and with some C# background you can go beyond.
In a few words, you can develop custom Dynamo nodes in C# quite simply, using the so-called “Zero Touch” method. This consists in compiling a DLL containing static methods: once you load it into Dynamo, these methods are exposed as nodes. Things could get more complicated if you want to build new elements: this needs a new class inheriting from Dynamo Element base class and new objects are created in Dynamo via static constructors. Again: nodes that require custom User Interface (UI) or have to react to some Revit events, as changes in document, must be built as explicit custom UI nodes. They have to implement NodeModel class and their DLL must be loaded at startup by Dynamo. In this kind of nodes you also need to build the abstract syntax tree (AST): this is essential for telling Dynamo how input data are processed to create outputs. From great powers comes great responsibilities!
Back to the main topic, I said that with Dynamo we can’t go inside a link model, so the “All Elements of Category” node is useless. This method queries elements with a FilteredElementCollector class on current Revit document. Current document is accessed through the CurrentDBDocument property of Dynamo DocumentManager instance but, considering for example walls, there is no wall inside this document:
public static IList<Element> OfCategory(Category category)
{
    if (category == nullreturn null;

    var catFilter = new ElementCategoryFilter(category.InternalCategory.Id);
    var fec = new FilteredElementCollector(DocumentManager.Instance.CurrentDBDocument);
    var instances = 
        fec.WherePasses(catFilter)
            .WhereElementIsNotElementType()
            .ToElementIds()
            .Select(id => ElementSelector.ByElementId(id.IntegerValue))
            .ToList();
    return instances;
}
I solved the problem defining:
  • a new element class in Dynamo called RevitLinkInstance
  • a OfCategoryLinkInstance query method
  • a custom UI node using the query method

RevitLinkInstance class

Not every Revit element has a corresponding class in Dynamo, as well as for Revit links. A new non-static class can store static methods and properties related to links, moreover it defines a type that can be used to wrap a Revit link instance into a Dynamo Revit link object. If no specific class is provided, Dynamo handles this elements as UnknownElement, a class inheriting from Element.
RevitLinkInstance class includes basic implementation for a Dynamo elements, plus Document property that returns the document associated with the link: this is a key point for querying elements in a link.
using DynamoServices;
using DB = Autodesk.Revit.DB;
using ZT = MBztn.Elements;

namespace MBztn.Elements
{
    [RegisterForTrace]
    public class RevitLinkInstance : ZT.Element
    {

        #region Internal properties

        internal DB.RevitLinkInstance InternalRevitLink
        {
            get;
            private set;
        }

        public override DB.Element InternalElement
        {
            get { return InternalRevitLink; }
        }

        #endregion

        #region Public Properties

        public MBztn.Application.Document Document
        {
            get { return MBztn.Application.Document
.FromExisting(InternalRevitLink.GetLinkDocument()); }
        }
        
        #endregion

        #region Private Mutators

        /// <summary>
        /// Set the internal Element, ElementId, and UniqueId
        /// </summary>
        /// <param name="rvtLink"></param>
        private void InternalSetRevitLink(DB.RevitLinkInstance rvtLink)
        {
            InternalRevitLink = rvtLink;
            InternalElementId = rvtLink.Id;
            InternalUniqueId = rvtLink.UniqueId;
        }

        #endregion

        #region Private Constructors

        /// <summary>
        /// Create from an existing Revit Element
        /// </summary>
        /// <param name="rvtLink"></param>
        private RevitLinkInstance(DB.RevitLinkInstance rvtLink)
        {
            SafeInit(() => InitRevitLink(rvtLink));
        }

        #endregion

        #region Helpers for private constructors

        /// <summary>
        /// Initialize the element
        /// </summary>
        /// <param name="rvtLink"></param>
        private void InitRevitLink(DB.RevitLinkInstance rvtLink)
        {
            InternalSetRevitLink(rvtLink);
        }

        #endregion

        #region Internal static constructors

        internal static RevitLinkInstance FromExisting
(DB.RevitLinkInstance rvtLinkbool isRevitOwned)
        {
            return new RevitLinkInstance(rvtLink)
            {
                IsRevitOwned_ZT = isRevitOwned
            };
        }

        #endregion

        public override string ToString()
        {
            return this.InternalRevitLink.Name.ToString();
        }

       
    }
}

Query method

OfCategoryInLinkInstance gets elements by category and a link instance. It differs from original Dynamo query by the input of filtered element collector. Associated document can be obtained from link instance by GetLinkDocument() method. Link document, instead of current document, is then used into filtered element collector. Finally ById method gets element from their Ids an wraps them into Dynamo elements.
/// <summary>
/// Get all instances of a category from a link instance
/// </summary>
/// <param name="dynCat"></param>
/// <param name="ztLink"></param>
/// <returns></returns>
public static IList<DYN.Element> OfCategoryInLinkInstance
(DYN.Category dynCatZT.RevitLinkInstance ztLink)
{
    if (dynCat == null || ztLink == nullreturn null;

    var catFilter = new DB.ElementCategoryFilter(new DB.ElementId(dynCat.Id));

    DB.Document linkDoc = ztLink.InternalRevitLink.GetLinkDocument();

    var fec = new DB.FilteredElementCollector(linkDoc);

    var instances = fec
        .WherePasses(catFilter)
        .WhereElementIsNotElementType()
        .ToElementIds()
        .Select(id => ElementSelector.ById(idztLink.InternalRevitLink))
        .ToList();

    return instances;
}
If this method became directly a Dynamo node, it would work properly only at first Dynamo run. Every change in Revit model wouldn't be caught because this method comes from a static class that can't react to model changes, so node is never refreshed and element collection remains always the same. Dynamo doesn't refresh a node unless at least one input changes, so a first basic solution is a custom node with a dummy bool input: its function is providing a input change by switching from false/true when a node refresh is needed.
But there's better than that: a custom UI node.

LinkInstanceElementsOfCategory custom UI node

A custom UI node is different from ZeroTouch:
  • it must implement NodeModel class
  • constructor is no longer static
  • input and output ports must be defined explicitly
LinkInstanceElementsOfCategory inherits from a more specific class than NodeModel, that's ElementQueryBase. This requires implementation of BuildOutputAst method, which defines the logic for calculating output in Abstract Syntax Tree (AST). AST is the way Dynamo treats inputs when evaluating a node. Inside BuildOutputAst, the OfCategoryLinkInstance query is called through AstFactory.BuildFunctionCall.
ElementQueryBase constructor subscribes an eventhandler to ElementsUpdated event, raised by Dynamo due to changes in Revit document elements. The handler forces Dynamo to recalculate output with BuildOutputAst, so the query method is called again and filtered element collector is refreshed. With a ZeroTouch node all this wouldn't happen: node is built when inserted into Dynamo workspace with no callbacks when elements are added or deleted into Revit document.
Remeber that the query method must be defined in another assembly than the custom UI node is written in, otherwise it won't work.
[NodeName("All Link Instance Elements of Category")]
[NodeCategory("MBui.Selection")]
[NodeDescription("Get all elements of a category in a link instance")]
[InPortNames("Category","LinkInstance")]
[InPortDescriptions("Category""Revit Link Instance")]
[OutPortNames("Elements")]
[OutPortDescriptions("Elements")]
[IsDesignScriptCompatible]

public class LinkInstanceElementsOfCategory : ElementsQueryBase
{
    public LinkInstanceElementsOfCategory()
    {
        RegisterAllPorts();
    }


    public override IEnumerable<AssociativeNode> BuildOutputAst
        (List<AssociativeNode> inputAstNodes)
    {

        if (!HasConnectedInput(0|| !HasConnectedInput(1))
        {
            return new[] {AstFactory.BuildAssignment
(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode())};
        }

        var func = new Func<DYN.CategoryZT.RevitLinkInstanceIList<DYN.Element>>
(DynElementQueries.OfCategoryInLinkInstance);

        var functionCall = AstFactory.BuildFunctionCall(funcinputAstNodes);

        return new[] { AstFactory.BuildAssignment
(GetAstIdentifierForOutputIndex(0), functionCall) };
    }
}

Example: getting walls from links

Here's a federated model linking some Revit sample models.
Let's say that we want to get all walls in links in order to do some check on their parameters. Dynamo graph could be something like that:
It's true that the same result could have been reached with Revit schedules in host model, but it requires extra operations and has some limitations:
  • If you want to list shared parameters of elements in links, this parameters must be shared even with host model, so every change in links shared parameters must be replicated in federated model
  • Suppose that the same shared project parameter is used by several categories, referring to both system an loadable families. You can't get by with only one multi-category schedule, because system families won't be listed. This means a schedule for walls, floors, ceilings, ducts, pipes and so on
  • Schedules can list elements and their properties, allowing some basic analysis with calculated parameters and conditional format: it's obvious that it's not enough for model checking purposes, with Dynamo you can do much more than that.
Dynamo elements returned by "All Link Instance Elements of Category" can be used with standard Dynamo nodes without any limitation except for these cases:
  • some Dynamo nodes get the element document by CurrentDBDocument property of Dynamo DocumentManager: current document doesn't contain the element (but the link in which element is), so node will return an error. This can be solved by implementing a new node in which document is get by Document property of Revit Element class
  • operations performing modifications in a link document are not allowed: this is obvious, because we can't modify a link in the same Revit application where its host model is opened.