How To Create Your Own Nodes

Michael Dewberry edited this page Sep 23, 2016 · 15 revisions
Clone this wiki locally

How To Create Your Own Nodes

So Many Nodes

Dynamo has a large assortment of core nodes that you can rely on to build pretty much any graph you need, but sometimes a quicker, more elegant, and more easily shared solution is to build your own nodes. These can be reused among different projects, they make your program clearer and cleaner, and they can be pushed to the package manager and shared.

So Many Ways

Dynamo also offers three different methods for importing and creating custom nodes. Each method gives you some more control of how the node interacts with the graph, and they get increasingly complex to build. For most day to day work you'll probably want to stick with custom nodes built directly in dynamo using the UI or Zero Touch importing, which lets you import a .dll of c# code that will get turned automatically into a series of nodes. The last option is to build explicit custom nodes in c#. Nodes built this way have the most flexibility, you can create custom UI's, respond to other nodes, or affect the state of the graph.

Method 1: Custom Nodes

Custom Node usually refers to a node that was built in the Dynamo UI and was constructed by nesting other nodes and custom nodes inside of a container. When this container node is executed in your graph, everything inside it will be executed. You can even place instances of this same node inside of itself to build recursive custom nodes!

To build a custom node you'll need to either start a new custom node or select some existing nodes in your dynamo graph, right click on the canvas, and hit node from selection.

Once you're in the yellow background canvas, you know you're working inside of a custom node. Custom nodes require a few parts:

Inputs - input nodes create input ports on this custom nodes whenever it is placed, strictly speaking these are not necessary, since you could build a custom node that only has an output. Like a constant value.

If you want to be able to use "lacing" properties on your nodes, be sure to identify the data type in the input. Also, you can add default values. Syntax is input name : datatype = default value

Outputs - these nodes are necessary and dynamo will create outputs automatically for you if you don't specify one. It's good practice to define these explicitly. Similar to inputs, these will create outports on the custom node when it's used in another graph.

Use

Functions!?

In Dynamo, many nodes are functions, this means they have some input, and they produce some output. Your custom nodes are functions too! This means you can map them over a list, use them to sort a list, or filter a list, they can even be the function you pass to reduce. These are all built-in nodes that take functions as one of their inputs.

You can tell that a node is acting as a function if its title bar is light grey. This means that one of its input arguments is left blank. For instance when we map this function over a list, we'll execute the custom node once for each item in the list and each time the current item will get set to that input. Thats the super quick functional programming intro - for more information check out the function passing sample in the help menu of Dynamo.

Recursion

...

Sharing

Any set of custom nodes you design and build in the UI can be uploaded to the package manager and shared. You can supply tags for others to search for it, and you can upload different versions to make fixes and changes. Any type of node can be shared on the package manager.

Method 2: "Zero Touch" Nodes: Automatically Generating Nodes From Code

This method is internally referred to as Zero Touch Import. The idea is that you can use C# to code a series of types, static methods, and static constructors in your own, seperate project file in an IDE like Visual Studio. You create a new library project (a .dll) and add your code there. When you compile the result is a .dll, a dynamically linked library. As long as your library is .NET-compliant it will get automatically converted into nodes and exposed inside of dynamo when you import the library.

TODO (Mike/Patrick) Fold in https://github.com/DynamoDS/Dynamo/wiki/Zero-Touch-Plugin-Development

Method 3: Nodes With Custom UI

**NOTE: ** See Matteo Cominetti's custom UI node example at https://github.com/teocomi/HelloDynamo for a more thorough presentation of this material.

You might want to create a custom UI for displaying an image, building some kind of interactivity, like a slider or dropdown box, or getting some feedback about the state of the node.

The ColorRange node is an example of a node that requires a custom UI. It draws the specified color range gradient into the dynamo graph. We’ll be looking at this node as an example of a custom UI node. The portions of code below are pulled directly from the dynamo source code. The src of this node is located at Dynamo\src\Libraries\CoreNodesUI\ColorRange.cs and Dynamo\src\Libraries\CoreNodesUI\UI\ColorRangeNodeViewCustomization.cs

When creating nodes that require custom UI, exposing static constructors and methods won’t work. We need to implement a NodeModel class to represent the core logic of the node and provide a class implementing INodeViewCustomization<ColorRange>, which handles building the WPF-specific user interface logic.

If you want to follow along in your own project, you'll want to create a new C# Library project in Visual Studio, and use the NuGet package manager to add the DynamoVisualProgramming.WpfUILibrary package to your project. Then add new C# Class items ColorRange.cs and ColorRangeNodeViewCustomization.cs. (You can read more about NuGet here.)

On ColorRangeNodeViewCustomization, we need to implement:

CustomizeView(ColorRange node, NodeView view) to build our custom UI using Windows Presentation Foundation (WPF)

Dispose() this gives us the ability to remove any event callbacks we've created as part of our UI.

On our implementation of NodeModel, we need to implement:

BuildOutputAst(List inputAstNodes) where you transform your inputs into your outputs and wrap them in the correct node data types,

DispatchOnUIThread(delegate) which will call the code you use to update the UI of your node. WPF requires that you only make changes to data bound to the UI on the UI thread. This method lets you make sure you execute those changes on the UI thread.

UI is created using WPF elements. The initial UI is built with c# in the method CustomizeView(ColorRange node, NodeView view). Here you’ll usually add panels, buttons, and dropdowns to the inputgrid element of your node’s view, you can think of this as the root of the node’s UI.

Besides overriding these methods, the other major difference is the constructor, which is no longer static. in and out ports must be defined explicitly, as can be seen below in the constructor ColorRange(), which takes no parameters, this is unlike zero touch nodes.

    [IsDesignScriptCompatible]
    [NodeName("Color Range")]
    [NodeCategory("Core.Color.Create")]
    [NodeDescription("Get a color given a color range.")]
    public class ColorRange : NodeModel
    {
        public event EventHandler RequestChangeColorRange;
        protected virtual void OnRequestChangeColorRange(object sender, EventArgs e)
        {
            if (RequestChangeColorRange != null)
                RequestChangeColorRange(sender, e);
        }

This constructor takes no parameters, and defines input ports and output ports explicitly. It also subscribes an eventhandler that responds to changing properties of the node. This can be used so that the color gradient that is displayed from this node is updated only when necessary, and can be done on the UI thread.

        public ColorRange()
        {
            InPortData.Add(new PortData("start", "The start color."));
            InPortData.Add(new PortData("end", "The end color."));
            InPortData.Add(new PortData("value", "The value between 0 and 1 of the selected color."));
            OutPortData.Add(new PortData("color", "The selected color."));

            RegisterAllPorts();

            this.PropertyChanged += ColorRange_PropertyChanged;
        }

        void ColorRange_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName != "IsUpdated")

                return;

            if (InPorts.Any(x => x.Connectors.Count == 0))
                return;

           OnRequestChangeColorRange(this, EventArgs.Empty);
        }

BuildOutputAst is where the outputs of this node are calculated. This method is used to do the work that a compiler usually does by parsing the inputs List inputAstNodes into an abstract syntax tree. All nodes are converted to an AST, this is just a different representation of what Dynamo will do with the inputs when it comes time to evaluate this node. Unlike the evaluate method in previous versions of Dynamo, no evaluation happens in this method.

Using the AstFactory class a function call is constructed that creates a color node, it calls the function by class.function name, and the arguments are passed as a list. Then this function call result is set to an output port. The Function BuildColorFromRange is defined on the Color class in CoreNodes.

        public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
        {
            var functionCall = AstFactory.BuildFunctionCall("Color", "BuildColorFromRange", inputAstNodes);

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

ColorRangeNodeViewCustomization.CustomizeView is where we can define WPF elements and bindings to data. It is important to read up on WPF and data bindings if you’re not already familiar. Most nodes define their UI through c# directly instead of using XAML files.

        public void CustomizeView(ColorRange model, NodeView view)
        {
            var drawPlane = new Image
            {
                Stretch = Stretch.Fill,
                HorizontalAlignment = HorizontalAlignment.Stretch,
                Width = 100,
                Height = 200
            };

            view.inputGrid.Children.Add(drawPlane);

The code to generate a dropdown box and bind it to some list might look like this. In the code below we bind the combobox’s ItemSouceProperty to a list of items. Now the combobox dropdown will populate with these items. We also bind the selected index of the dropdown so we can store the selection. This combobox could then be added to the inputgrid of the node.

var combo = new ComboBox
                        {
                            Width = System.Double.NaN,
                            MinWidth = 100,
                            Height = Configurations.PortHeightInPixels,
                            HorizontalAlignment = HorizontalAlignment.Stretch,
                            VerticalAlignment = VerticalAlignment.Center
                        };
                sp.Children.Add(combo);
             //set the data context for this combobox to this NodeModel
            combo.DataContext = model;
            //bind this combo box to the selected item hash
            var bindingVal = new System.Windows.Data.Binding("Items") 
                              { Mode = BindingMode.TwoWay, Source = model };
            combo.SetBinding(ItemsControl.ItemsSourceProperty, bindingVal);

            //bind the selected index to the selected index property of the index wrapper we just created above
            var indexBinding = new Binding()
            {
                Path = new PropertyPath("SelectedIndex"),
                Mode = BindingMode.TwoWay,
                Source = new_selected_index
            };
            combo.SetBinding(Selector.SelectedIndexProperty, indexBinding);

            return combo;

In some cases, the UI you’re building may need to not only accept input, but give feedback based on its incoming data. We can use the dynamo EngineController to lookup the data associated with a specific port when the graph is run. Then we can use that data to alter our databound UI and WPF will update our elements as needed.

Our callback for the color needing to be updated is dispatched on the UI thread here. The code below uses GetMirror(), which is method to get the data associated with a specific port on a specific node. We use the incoming color objects to generate a new gradient bitmap, which is displayed in the UI.

NOTE GetMirror() lets you access other nodes in the graph, but this is most likely not safe, you should limit your use of GetMirror() to read the input values to the current node, like is done below to generate an image to display in the node UI

            RequestChangeColorRange += delegate
            {
                DispatchOnUIThread(delegate
                {
                    var colorStartNode = InPorts[0].Connectors[0].Start.Owner;
                    var startIndex = InPorts[0].Connectors[0].Start.Index;
                    var colorEndNode = InPorts[1].Connectors[0].Start.Owner;
                    var endIndex = InPorts[1].Connectors[0].Start.Index;

                    var startId = colorStartNode.GetAstIdentifierForOutputIndex(startIndex).Name;
                    var endId = colorEndNode.GetAstIdentifierForOutputIndex(endIndex).Name;

                    var startMirror = dynSettings.Controller.EngineController.GetMirror(startId);
                    var endMirror = dynSettings.Controller.EngineController.GetMirror(endId);

                    object start = null;
                    object end = null;

                    if (startMirror.GetData().IsCollection)
                    {
                        start = startMirror.GetData().GetElements().Select(x => x.Data).FirstOrDefault();
                    }
                    else
                    {
                        start = startMirror.GetData().Data;
                    }

                    if (endMirror.GetData().IsCollection)
                    {
                        end = endMirror.GetData().GetElements().Select(x => x.Data).FirstOrDefault();
                    }
                    else
                    {
                        end = endMirror.GetData().Data;
                    }

                    Color startColor = start as Color;
                    Color endColor = end as Color;
                    if (null != startColor && null != endColor)
                    {
                        WriteableBitmap bmp = CompleteColorScale(startColor, endColor);
                        drawPlane.Source = bmp;
                    }
                });
            };
        }