Zero Touch Plugin Development

Michael Dewberry edited this page Sep 22, 2016 · 35 revisions
Clone this wiki locally

This page outlines the process of developing a custom Dynamo node in C# using the "Zero Touch" interface

An example Visual Studio 2012 project can be found here: https://github.com/DynamoDS/ZeroTouchEssentials

Note 1: Make sure your Visual Studio project's "Platform target" is set to "Any CPU" or x64, and "Prefer 32-bit" is un-checked.

Note 2: If you are creating Geometry objects in your ZeroTouch nodes, see a VERY IMPORTANT note at the bottom.

Basics

In most cases, C# static methods and Classes can be imported without modification. If your library only needs to call functions, and not construct new objects, this can be achieved very easily with static methods. When Dynamo loads your DLL, it will strip off the namespace of your classes, and expose all static methods as nodes. For instance, we can create a node that takes a single number and multiplies it by 2 as follows:

    namespace ZeroTouchExample
    {
        public class ZeroTouchExample
        {
            public static double MultByTwo(double inputNumber) 
            {
                return inputNumber * 2.0;
            }
        }
    }

A node will appear in the Dynamo menu called "MultByTwo" in the "ZeroTouchExample" category. It will have one input, called "inputNumber".

Default values

Dynamo can support having default values for input ports on the node. These are the values that will be supplied to the node if the ports aren't wired up.

They can be expressed with the C# way of specifying optional arguments (https://msdn.microsoft.com/en-us/library/dd264739.aspx).

    public static double Pow(double inputNumber, double power = 2.0) 
    {
        return Math.Pow(inputNumber, power);
    }

Currently the default types that are supported are numbers, bools and Strings, like in C#. We're working on expanding the set of support items soon.

Returning multiple values

Accepting multiple inputs into a node is easy: just specify multiple parameters to your C# function. Returning multiple values from a node requires slightly more work. First step is to reference "DynamoServices.dll" in your project, found in the Dynamo install location, and add "using Autodesk.DesignScript.Runtime;" to the top of your C# script. The second step is to add the "MultiReturn" attribute to your function. It should contain an array of strings, containing the names of the output ports. Your function should then return a Dictionary containing the returned values. The keys to your dictionary should match the parameter names in your attribute. For example:

    using Autodesk.DesignScript.Runtime;

    namespace ZeroTouchExample
    {
        public class ZeroTouchExample
        {

            [MultiReturn(new[] { "add", "mult" })]
            public static Dictionary<string, object> ReturnMultiExample(double a, double b)
            {
                return new Dictionary<string, object>
                {
                    { "add", (a + b) },
                    { "mult", (a * b) }
                };
            }
        }
    }

This creates a new node that outputs two doubles, "add" and "mult" containing the addition and multiplication of the two input numbers.

Objects

Dynamo doesn't have a "new" keyword, so objects need to be constructed via static constructors. Dynamo uses the "By" prefix to indicate a static method is a constructor, and while this is optional, using "By" will help your library better fit into the existing Dynamo style.

    namespace ZeroTouchExample
    {
        public class MyLine
        {
            private double _x1;
            private double _y1;
            private double _x2;
            private double _y2;

            public static MyLine ByStartPointEndPoint(double x1, double y1, double x2, double y2)
            {
                _x1 = x1;
                _y1 = y1;
                _x2 = x2;
                _y2 = y2;
            }

            public double Length()
            {
                return Math.sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2));
            }
        }
    }

Using Dynamo geometry types

Dynamo libraries can also use native Dynamo geometry types as inputs, and create new geometry as outputs. The first step is to reference "ProtoGeometry.dll" in your project, and include "using Autodesk.DesignScript.Geometry;" at the top of your C# file. Dynamo geometry objects are then used like any other passed object to your functions. For example, we can clean up our MyLine example above by using Dynamo's native geometry types:

    using Autodesk.DesignScript.Geometry;

    namespace ZeroTouchExample
    {
        public class MyLine
        {
            private Autodesk.DesignScript.Geometry.Point _p1;
            private Autodesk.DesignScript.Geometry.Point _p2;

            public static MyLine ByStartPointEndPoint(Autodesk.DesignScript.Geometry.Point p1, Autodesk.DesignScript.Geometry.Point p2)
            {
                _p1 = p1;
                _p2 = p2;
            }

            public double Length()
            {
                return Math.sqrt(Math.Pow(_p2.X- _p1.X, 2) + Math.Pow(_p2.Y - _p1.Y, 2));
            }
        }
    }

Documentation, Tooltips, and Search

It's best practice to add documentation to your Dynamo nodes. This is done through XML documentation tags. The main documentation for your node should be in the <summary>...</summary> XML tags. This will appear as a tooltip over your node in the left search side bar. Documentation on specific input parameters should appear in the <param name="inputName">...</param> XML tag.

You can specify the name of the output parameter with the <returns>...</returns> XML tag. Add a "name" attribute with the output name to the tag (as shown belo). The tag should contain the tooltip of the output port. For multiple output zero touch methods, simply add more "returns" fields:

/// <returns name="foo">The first output</param>
/// <returns name="bar">The second output</param>

Control over how your node appears in the Dynamo search results is similarly controlled through the <search>...</search> XML documentation tag. This tag should contain a comma-separated list of search terms that, if matched, will cause your node to appear in search results.

To get Dynamo to pickup the tags you must enable XML documentation generation through visual studio, this setting is in the Build tab of the project settings.

We can add documentation an search to our original example as follows:

    namespace ZeroTouchExample
    {
        public class ZeroTouchExample
        {
            /// <summary>
            /// This is an example node demonstrating how to use the Zero Touch import mechanism.
            /// It returns the input number multiplied by 2.
            /// </summary>
            /// <param name="inputNumber">Number that will get multiplied by 2</param>
            /// <returns name="outputNumber">The result of the input number multiplied by 2</param>
            /// <search>
            /// example, multiply, math
            /// </search>
            public static double MultByTwo(double inputNumber) 
            {
                return inputNumber * 2.0;
            }
        }
    }

Migrations

As you publish newer versions of your library, you may want to change the names of ZeroTouch nodes. It is possible to specify these name changes in a migrations file so that graphs built on previous versions of your library don't break when you create an update.

Migration files should be named in the following format: BaseDLLName.Migrations.xml. For instance ProtoGeometry.dll, Dynamo's core geometry library, has a migration file named ProtoGeometry.Migrations.xml. The file needs to be located in the same directory as the base dll.

The migrations file should be valid XML. The file should contain one "migrations" element, containing several "priorNameHint" elements. Each prior name element should have one "oldName" element, and one "newName" element. Old name should contain the complete name, including namespace, of the previous method name. newName should contain the new name, also including complete namespace. See ProtoGeometry.Migrations.xml for a template migrations file.

Note that in its current state, the migrations file gives hints to Dynamo about up how to handle missing nodes in a DLL, but doesn't specify in absolute terms a mapping between versions. So, for instance, if you "depreciate" a node name, then begin re-using the name in a subsequent version, the migration will likely fail.

Dispose / using Statement

If you are using the Dynamo geometry library in your C# plugins, you'll need to manually manage the geometry resources created in your functions which are not returned out of your functions. Any resources returned out of your functions will be managed by the Dynamo engine. The are two ways to do this, a "using" statement, or manual Dispose() calls. The using statement is documented here.

IF YOU DO NOT IMPLEMENT THIS YOU WILL PUT BOTH DYNAMO AND REVIT INTO AN UNDEFINED STATE CAUSING BOTH TO MYSTERIOUSLY CRASH!!!

Here are two examples:

With using

    using (Point p1 = Point.ByCoordinates(0, 0, 0))
    {
        using (Point p2 = Point.ByCoordinates(10, 10, 0))
        {
            return Line.ByStartPointEndPoint(p1, p2);
        }
    }

With Dispose

    Point p1 = Point.ByCoordinates(0, 0, 0);
    Point p2 = Point.ByCoordinates(10, 10, 0);
    Line l = Line.ByStartPointEndPoint(p1, p2);
    p1.Dispose();
    p2.Dispose();
    return l;

Can I use Generics ?

So short answer is no, not currently in the zero touch importer, but you can of course use generics in your code, just not in the code that is directly imported where the type is not set.

so if you try to write a zero touch node of type T, like

 public class SomeGenericClass<T>
    {
        public SomeGenericClass()
        {
            Console.WriteLine(typeof(T).ToString());
        }

    }

this will NOT be imported - but the rest of the library should be, so you will get missing type exceptions

If you use a generic type with the type set like:

  public class SomeWrapper
   {
        public object wrapped;
        public SomeWrapper(SomeGenericClass<double> someContrainedType)
    {
            Console.WriteLine(this.wrapped.GetType().ToString());
    }
   }

this second class should import fine. You wouldn’t be able to expose any methods, properties, or classes, that are generic and don’t have the type set.

This constructor would be kind of useless though since you can’t construct the SomeGenericClass object - you would need another method that outputs that type.