Skip to content

Getting Started with Dynamo Development

Fabian Ritter edited this page Aug 2, 2013 · 15 revisions

Development Guide


This page is outdated! It will be refreshed once some new changes to the node API are in place.


##The Spirit of Open-Source Development

Because Dynamo is open-source on GitHub, anyone can make a copy of the code and modify it however they like. Since Dynamo is an entire programming language and is very much a Work in Progress, contributions to the source code from the community is strongly encouraged and is almost entirely necessary for it's success.

Users can contribute by simply writing nodes which they feel should be included out of the box. At this time, a very small subset of the Revit API has been exposed in Dynamo, so we hope that users familiar with the Revit API can contribute in order to expand that domain as well.

##Pre-requisites

###Revit 2013

In order to work on the parts of Dynamo that use Revit, you will need to have some full-flavored variant of Revit 2013. It's expensive, but it you're a student or educator you can download it for free here.

###IronPython

In order to work with the parts of Dynamo that use IronPython, you will need to install IronPython, which you can download here.

##The Dynamo Visual Studio Solution

Dynamo is written in a combination of C# and F#, two .NET languages. While you can use any Visual Studio version (2008 and onwards) to work with the Dynamo Source, Visual Studio will need to be configured to be able to load and build C# and F# projects.

###DragCanvas The DragCanvas project contains a WPF component based on the Canvas class, which allows UIElements to be dragged around a canvas using a mouse.

###DynamoElements DynamoElements is the main Dynamo project, and is (probably) the one you will be spending most of your time in. DynamoElements contains the entire Dynamo UI, all Node defintions, and the program logic. ####Notable Files:

  • dynBench.xaml/dynBench.xaml.cs - Contains the UI and logic behind it.
  • dynNode.xaml/dynNode.xaml.cs - Contains the UI and logic for Nodes.
  • dynBaseTypes.cs - Contains most of the basic nodes.

###DynamoRevit DynamoRevit contains the plugin code for Revit and Vasari. It is where the Ribbon button is setup and where initialization takes place for the UI and the Dynamic Model Updater.

###FScheme The FScheme project serves as the foundation for running Dynamo scripts. It's what handles the order of execution and passing arguments to nodes as inputs. In reality, FScheme is a programming language based on an existing language called Scheme. FScheme is written in F# due to a close relationship with the language (both FSharp and FScheme are functional programming languages), and because F# has Tail Call Optimizations built into the compiler, which allows for the construction of recursive calls of unlimited depth without worrying about stack overflows.

###FSchemeInterop FSchemeInterop contains wrappers which are used to quickly convert C# code to code blocks executable from FScheme. Odds are, you will be using some utility functions contained in this project when writing new nodes.

##Writing your first new Node

###Getting Setup First thing's first: make sure that Dynamo is building properly before you start to modify it. You can start a build by clicking Build > Build Solution (or by pressing F6 by default). If it says "Build Successful" at the bottom of the screen, then you're good to go! If not, then you will see some errors which need to be fixed.

Assuming you're now building correctly, we're ready to write a node! Make a new file called MyFirstNode.cs in the DynamoElements project (since that's where Nodes go).

First, you'll want to tweak the auto-generated code. Your class MyFirstNode has to extend dynNode in order to be a Node. You will also want to add a few using statements:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.FSharp.Collections;
using Dynamo.Connectors;
using Value = Dynamo.FScheme.Value;

namespace Dynamo.Elements
{
 class MyFirstNode : dynNode
 {
 }
}

The two extra using statements are necessary for proper code-style in the Dynamo project. They are also fairly convenient and will save you a lot of unnecessary typing.

###Defining the Node

In this example, we will be creating a node that takes the maximum of two given numbers. So this node will take in two numbers as inputs, and return one number as a result. In Dynamo, it's not necessary for any node to take in input, but ALL nodes must have one and only one output.

First, we need to add some Attributes to the class definition:

[ElementName("Max")]
[ElementCategory(BuiltinElementCategories.MATH)]
[ElementDescription("Returns the maximum of two given numbers.")]
[RequiresTransaction(false)]
class MyFirstNode : dynNode
{
}

While these aren't the only attributes you can use for a node, these are the ones you should use for every node you define.

  • ElementName(string) - The name of your node, this must be unique. It will also be what the user sees in Dynamo.
  • ElementCategory(string) - The category this node belongs to in the side-menu. You can put whatever string you like here, and if the category doesn't already exist, it will be created for the node. If you want to reference any of the built-in categories, however, it is recommended you reference it via the BuiltInElementCategories class. This is important if, in the future, the names of the categories change. If you reference these instead of hard-coding the string, you won't have to update them in the future if they change.
  • ElementDescription(string) - Description of the node, displayed as a ToolTip to the user when they mouse over the node.
  • RequiresTransaction(bool) - True or false value determining whether or not the Node's logic should be wrapped in a Revit Transaction. If your node calls any API that requires a Transaction, then set this to true. Otherwise set it to false for performance purposes.

We define our inputs and output in the constructor like so:

public MyFirstNode()
{
 InPortData.Add(new PortData("num1", "A number to compare", typeof(double)));
 InPortData.Add(new PortData("num2", "A number to compare", typeof(double)));
 OutPortData = new PortData("max", "The greater of the two inputs", typeof(double));

 NodeUI.RegisterAllPorts();
}

Let's take an in-depth look at this. InPortData is a field inherited from dynElement that is a list of PortData object, used to define the inputs of the Node. OutPortData is a field inherited from dynElement that is a single PortData object, used to define the output of the node. PortData is a class used to define the characteristics of any Port (either Input or Output).

The arguments to PortData in order are:

  • string nickName - The name of the port. This will be displayed as text visually on the node.
  • string top - Description of the port. This will appear as a ToolTip when the user overs over the port.
  • Type portType - Type of object the port produces/accepts. Right now this functionality is not implemented into Dynamo, but eventually this will be used to enforce Type-checking automatically, so that the user cannot connect two nodes without matching types.

Finally, NodeUI.RegisterAllPorts(); tells the UI to update it's layout so that it can display all of the Ports that have just been defined. This should always be called in the constructor after all of the ports have been defined.

###Writing the logic

The Node's logic is the code that will be called when the node is ready to be evaluated. You will have access to the inputs given to the node, and can use them to calculate the desired output.

The logic is contained in an overridden method inherited from dynNode called Evaluate. Add the following to your node definition underneath the constructor:

public override Value Evaluate(FSharpList<Value> args)
{
 //logic goes here
}

From the method signature, you can see that the logic takes in a List of Expression objects, and returns an Expression object.

####About Values

In Dynamo, all arguments and return values are objects of type Value. A Value is a container that is used to differentiate between datatypes under the hood in Dynamo's FScheme internals. Values come in a few flavors, here are the ones you need to know about:

  • Value.Number
  • Value.String
  • Value.List
  • Value.Container - Contains any other object.

To access the contents of an Value, you must first cast the Value to the appropriate Value type, and then get it's contents by using the Item field.

For example, if I want to get a number or a string out of a Value:

double a = ((Value.Number)numberValue).Item;
string s = ((Value.String)stringValue).Item;

Objects work a little differently: Item for Value.Container will return an object, so that must be cast to the specific type you want.

XYZ location = (XYZ)((Value.Container)objValue).Item;

Lists require a bit more explanation. Item for Value.List will return a FSharpList. Each element in the list is another expression, which then must be converted using the same rules as above.

//A quick way to convert a list using LINQ
var xyzSequence = ((Value.List)listValue).Item.Select(
 xyzExpr =>
 (XYZ)((Value.Container)xyzValue).Item
);
foreach (XYZ loc in xyzSequence)
{
 //process each XYZ
}

#####How do I know what the type of a Value is?

In the Evaluate method, any exceptions that are thrown are automatically caught by Dynamo and notified to the user. So it's usually common practice to simply cast to what you need and assume it works. Any failures will be announced to the user, and that means they haven't followed the input requirements for your node and will have to adjust their inputs.

However, there are situations where you may be expecting inputs of one type or another. In this case, you can use the Value.Is[Type] fields. For example:

if (value.IsNumber)
{
 //Treat value as a number
}
else if (value.IsString)
{
 //Treat value as a string
}
//...

####Convert the arguments

We first have to get our desired inputs from the argument list, like so:

double a = ((Value.Number)args[0]).Item;
double b = ((Value.Number)args[1]).Item;

Since we are expecting two inputs, bother numbers, we simply fetch the first two elements from the input list (args), cast them to Value.Number, and then pull the number using .Item.

####Generate the output

Now that we have the numbers, we can calculate the max:

double result = Math.Max(a, b);

And finally, we return the result in a new Value.Number:

return Value.NewNumber(result);

Note that we don't use the new keyword to construct a new Expression. All Expressions are constructed using the Value.New[Type] factory methods.

###And we're done

Here is a look at our new, finished node:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.FSharp.Collections;
using Dynamo.Connectors;
using Value = Dynamo.FScheme.Value;

namespace Dynamo.Elements
{
 [ElementName("Max")]
 [ElementCategory(BuiltinElementCategories.MATH)]
 [ElementDescription("Returns the maximum of two given numbers.")]
 [RequiresTransaction(false)]
 class MyFirstNode : dynElement
 {
 public MyFirstNode()
 {
 InPortData.Add(new PortData("num1", "A number to compare", typeof(double)));
 InPortData.Add(new PortData("num2", "A number to compare", typeof(double)));
 OutPortData = new PortData("max", "The greater of the two inputs", typeof(double));

 NodeUI.RegisterAllPorts();
 }
 }

 public override Value Evaluate(FSharpList<Value> args)
 {
 double a = ((Value.Number)args[0]).Item;
 double b = ((Value.Number)args[1]).Item;

 double result = Math.Max(a, b);

 return Value.NewNumber(result);
 }
}

So now we build DynamoElements, copy the new DynamoElements.dll file over to your Dynamo installation directory, and run Dynamo. You should see your new Max node under the Math section of the side menu. Test it out!

##Dynamo and Revit

To demonstrate the proper way to write Revit nodes in Dynamo, we will look at a simple example. This node will take in an XYZ and return a ReferencePoint located at that XYZ.

Here is how one might design it naively:

[ElementName("My Reference Point")]
[ElementCategory(BuiltinElementCategories.REVIT)]
[ElementDescription("An element which creates a reference point.")]
[RequiresTransaction(true)]
public class MyReferencePoint : dynNode
{
 public MyReferencePoint()
 {
 InPortData.Add(new PortData("xyz", "The point(s) from which to create reference points.", typeof(XYZ)));
 OutPortData = new PortData("pt", "The Reference Point(s) created from this operation.", typeof(ReferencePoint));

 NodeUI.RegisterAllPorts();
 }

 public override Value Evaluate(FSharpList<Value> args)
 {
 XYZ loc = (XYZ)((Value.Container)args[0]).Item;

 return Value.NewContainer(
 this.UIDocument.Document.FamilyCreate.NewReferencePoint(loc)
 );
 }
}

Now, if you were to run this once, it would work fine; when executed, this node will blindly make a new ReferencePoint at the location of the given XYZ.

However, you will notice that on subsequent runs, it creates new ReferencePoints and keeps the old ones where they were. With Run Automatically checked in Dynamo, this is problematic.

What we need to do is store a reference to the created ReferencePoint in our code, and then update that ReferencePoint's position on subsequent Evaluate calls.

All nodes inherit a special list from dynElement called Elements. When an ElementId is placed in the Elements list, Dynamo will automatically watch those ElementIds to make sure they remain valid on later runs. This saves the user a lot of effort, since they won't have to keep track of changes in the document and whether or not the Dynamo transaction was successful or rolled back.

Using Elements, we re-write the Evaluate method to look like this:

public override Value Evaluate(FSharpList<Value> args)
{
 XYZ loc = (XYZ)((Value.Container)args[0]).Item;

 ReferencePoint pt;

 //If we've made any elements previously...
 if (this.Elements.Any())
 {
 //Move existing...
 }
 //...otherwise...
 else
 {
 //...just make a point and store it.
 pt = this.UIDocument.Document.FamilyCreate.NewReferencePoint(loc);
 this.Elements.Add(pt.Id);
 }

 //Fin
 return Value.NewContainer(pt);
}

So now, we check to see if we've made anything by checking if we've put anything in Elements. If Elements is empty, we make the Element like normal, and then add it to Elements.

Now let's fill in the code for when we've already stored one.

...
//If we've made any elements previously...
if (this.Elements.Any())
{
 Element e;
 //...try to get the first one...
 if (dynUtils.TryGetElement(this.Elements[0], out e))
 {
 //..and if we do, update it's position.
 pt = e as ReferencePoint;
 pt.Position = loc;
 }
 else
 {
 //...otherwise, just make a new one and replace it in the list.
 pt = this.UIDocument.Document.FamilyCreate.NewReferencePoint(loc);
 this.Elements[0] = pt.Id;
 }
}
...

Elements stores ElementIds, so we need to fetch the Element from the document. dynUtils contains a utility method for doing this, called TryGetElement. We pass in the ElementId to fetch (taken directly from Elements), and we pass in the variable we want to assign the fetched element to. TryGetElement will return true if it was successful, and false if the ElementId is no longer valid.

So if it's successful, we just cast the element to a ReferencePoint, and then set the position to the new xyz. If it's unsuccessful, we create a new element like normal, and then update the stored Id in Elements.

Here is the completed Node:

[ElementName("My Reference Point")]
[ElementCategory(BuiltinElementCategories.REVIT)]
[ElementDescription("An element which creates a reference point.")]
[RequiresTransaction(true)]
public class MyReferencePoint : dynNode
{
 public MyReferencePoint()
 {
 InPortData.Add(new PortData("xyz", "The point(s) from which to create reference points.", typeof(XYZ)));
 OutPortData = new PortData("pt", "The Reference Point(s) created from this operation.", typeof(ReferencePoint));

 NodeUI.RegisterAllPorts();
 }

 public override Value Evaluate(FSharpList<Value> args)
 {
 XYZ loc = (XYZ)((Expression.Container)args[0]).Item;

 ReferencePoint pt;

 //If we've made any elements previously...
 if (this.Elements.Any())
 {
 Element e;
 //...try to get the first one...
 if (dynUtils.TryGetElement(this.Elements[0], out e))
 {
 //..and if we do, update it's position.
 pt = e as ReferencePoint;
 pt.Position = loc;
 }
 else
 {
 //...otherwise, just make a new one and replace it in the list.
 pt = this.UIDocument.Document.FamilyCreate.NewReferencePoint(loc);
 this.Elements[0] = pt.Id;
 }
 }
 //...otherwise...
 else
 {
 //...just make a point and store it.
 pt = this.UIDocument.Document.FamilyCreate.NewReferencePoint(loc);
 this.Elements.Add(pt.Id);
 }

 //Fin
 return Value.NewContainer(pt);
 }
}
Clone this wiki locally