NodeModelノードを作ってみよう!中級編

公開:2021/02/16
更新:2021/02/20
11 min読了の目安(約10200字TECH技術記事

概要

初級編ではZeroTouchノードの仕組みについて学びました。中級編は少しレベルアップをして、WPFを用いたカスタムUIノードを作成します!

WPFとは

WPFはXAML(Extensible Application Markup Language)という言語を用いてUIを開発することが出来るライブラリです。処理部分の実装はC#で記述して、フォーム部分とのデータバインディングを行うことが出来ます。

まず、WPFアプリを作成して、スライダーやチェックボックスの動作を確認しましょう。
File > New > Projectから、新しいWPFアプリプロジェクトを作成します。

MainWindow.xamlに以下のコードをコピー&ペーストしてください。チェックボックス、スライダー、ボタンが作成されます。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Width="200" SizeToContent="Height">
    <StackPanel>
        <CheckBox Name="EnabledCheckBox" Content="Enable" IsChecked="False" Margin="7"/>
        <StackPanel Orientation="Horizontal">
            <Slider x:Name="slider" Width="140" IsSnapToTickEnabled="True" TickFrequency="1" Minimum="0" Maximum="100" Foreground="Black" Margin="7"/>
            <TextBox Name="textbox" Text="{Binding ElementName=slider, Path=Value}" Margin="10" VerticalAlignment="Center"/>
        </StackPanel>
        <Button Width="75" Margin="5" HorizontalAlignment="Left" VerticalAlignment="Top" Click="Button_Click" Content="Run" IsEnabled="{Binding ElementName=EnabledCheckBox, Path=IsChecked}" />
    </StackPanel>
</Window>

コードビハインド

コードビハインドは、GUI のイベント処理部分を別ファイルで管理する方法です。
実際に、ボタンがクリックされた時にスライダー値を表示する以下の関数を、MainWindow.xmal.csに追加しましょう!

private void Button_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("値は" + slider.Value + "です!");
        }

アプリケーションをスタートして以下の画像が出てきたら成功です!!

ユーザーコントロール

ユーザーコントロールとは、コンポーネントを組み立てて、大きな一つのコンポーネントとして扱う的なものです。
Project > Add > UserControlをクリックして、ユーザーコントロールを作成します。名前はMyCustomControl.xamlとします。

MainWindow.xamlの<StackPanel>...</StackPanel>の内容をMyCustomControl.xamlの内容に置き換えます。MainWindow.xaml.csのButton_Click関数も同様にして、MyCustomControl.xaml.csに移動します。

これで、他のWPFコントロールの中に埋め込むことができる、再利用可能なカスタムコントロールを作成しました。
MainWindow.xamlでは、<StackPanel>...</StackPanel><local:MyCustomControl />を追加します。

NodeModelノードとは

NodeModelベースのノードは Zero-Touchノードとの大きな違いがいくつかあり、Zero-Touch ノードよりも大幅に柔軟性とパワーを提供してくれます。

Dynamoの設計思想

DynamoはMVVM(model-view-view-model)ソフトウェアアーキテクチャパターンに基づいており、UIをバックエンドから分離しています。ZeroTouchノードを作成する際、DynamoはノードのデータとUIの間のデータバインドを行います。カスタムUIを作成するには、データバインドロジックを追加する必要があります。


MVVMの図

  1. ノードのコアロジックを確立するためのNodeModelクラス
  2. NodeModel の表示方法をカスタマイズするための INodeViewCustomizationクラス

NodeModelを実装しよう!

それでは、カスタムUIノード内でUser Controlを使用する方法を見てみましょう。初級編と同様に、クラスライブラリプロジェクト(DLL) を作成します。名前はDynamoUnchained.ExplicitNode とします。NuGet経由でWpfUILibraryを追加でインストールします。
HelloUI.cs(NodeModelクラス)とHelloUINodeView(INodeViewCustomizationクラス)を追加します。

NodeModelノードは、ZeroTouchノードといくつかの重要な違いがあります。NodeModelノードは関数のみを呼び出すことができるため、NodeModelと関数を異なるライブラリに分離する必要があります。なので以下のようなノードの処理に関するコードは、個別のプロジェクトに作成します。

public static string SayHello(string Name)
    {
      return "Hello " + Name + "!";
    }

File > Add > NewProjectからクラスライブラリを追加します。名前は、DynamoUnchained.ExplicitNode.Functionsとします。このFunctionsクラスに先ほど説明したノードの処理にあたる部分を書いていきます。

早速NodeModelを実装していきます!HelloUI.csに、以下のコードをコピー&ペーストしてください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dynamo.Graph.Nodes;
using ProtoCore.AST.AssociativeAST;

namespace DynamoUnchained.ExplicitNode
{
    [NodeName("HelloUI")]
    [NodeDescription("Sample Explicit Node")]
    [NodeCategory("DynamoUnchained")]
    [InPortNames("A")]
    [InPortTypes("double")]
    [InPortDescriptions("Number A")]
    [OutPortNames("Output")]
    [OutPortTypes("double")]
    [OutPortDescriptions("Product of two numbers")]
    [IsDesignScriptCompatible]
    public class HelloUI : NodeModel
    {
        public HelloUI()
        {
            RegisterAllPorts();
        }

        private double _sliderValue;

        public double SliderValue
        {
            get { return _sliderValue; }
            set
            {
                _sliderValue = value;
                RaisePropertyChanged("SliderValue");
                OnNodeModified(false);
            }
        }

        public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
        {
            if (!InPorts[0].IsConnected)
            {
                return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode()) };
            }
            var sliderValue = AstFactory.BuildDoubleNode(SliderValue);
            var functionCall =
              AstFactory.BuildFunctionCall(
                new Func<double, double, double>(DynamoUnchained.ExplicitNode.Functions.Functions.MultiplyTwoNumbers),
                new List<AssociativeNode> { inputAstNodes[0], sliderValue });

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

    }
}

各部分で何が行われているのか理解しましょう。

[NodeName("HelloUI")]
[NodeDescription("Sample Explicit Node")]
[NodeCategory("DynamoUnchained")]
[InPortNames("A")]
[InPortTypes("double")]
[InPortDescriptions("Number A")]
[OutPortNames("Output")]
[OutPortTypes("double")]
[OutPortDescriptions("Product of two numbers")]
[IsDesignScriptCompatible]

名前、カテゴリ、InPort / OutPort名、InPort / OutPortタイプ、説明などのノード属性を指定します。

public class HelloUI : NodeModel
{

}

カスタム UI ノードはネイティブノードと同じように NodeModel インターフェースを実装します。インターフェースを実装するクラスには、クラス名の後に :NodeModel を追加し、クラス内に特定の機能を持たせる必要があります。

public HelloUI()
{
    RegisterAllPorts();
}

ノードの入力と出力を登録するコンストラクターです。

private double _sliderValue;
    public double SliderValue
        {
            get { return _sliderValue; }
            set
            {
                _sliderValue = value;
                RaisePropertyChanged("SliderValue");
                OnNodeModified(false);
            }
        }

ここではスライダーが変化したときに値を更新する設定をしています。

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

}

BuildOutputAst()は、NodeModelノードからデータを返すために必要な構造であるAST(抽象構文木)を返します。

if (!InPorts[0].IsConnected)
    {
        return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode()) 
    };	

入力ポートが接続されていない場合、nullを返します。これによって、ノードに警告が表示されないようになります。

var sliderValue = AstFactory.BuildDoubleNode(SliderValue);

スライダーの値を表すノードをAST内に作成します。

var functionCall =
              AstFactory.BuildFunctionCall(
                new Func<double, double, double>(DynamoUnchained.ExplicitNode.Functions.Functions.MultiplyTwoNumbers),
                new List<AssociativeNode> { inputAstNodes[0], sliderValue });
return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall) };

Functions.csから MultiplyTwoNumbers関数を呼び出します。

関数を呼び出そう!

ここでは、入力ポートにつながれた数値とスライダーをかけて、その値を出力するという処理を書きます。NuGetパッケージマネージャーからDynamoVisualProgramming.DynamoServicesをインストールしましょう。Functions.csに以下のコードをコピー&ペーストします。

using Autodesk.DesignScript.Runtime;
namespace DynamoUnchained.ExplicitNode.Functions
{
  [IsVisibleInDynamoLibrary(false)]
  public static class Functions
  {
    public static double MultiplyTwoNumbers(double a, double b)
    {
      return a * b;
    }
  }
}

CustomNodeModel はMultiplyTwoNumbers関数を呼び出すために Functions.csを参照する必要があります。
Add > Reference > Project > solution > Functions.csにチェックを入れ、OKを押します。

UIをカスタマイズしよう!

先ほど作成した以下のファイルを、DynamoUnchained.ExplicitNodeにドラッグ&ドロップします。


また、これら2つのファイルの名前空間をWpfAppからDynamoUnchained.ExplicitNodeに置き換える必要があります。

INodeViewCustomizationを実装しよう!

INodeViewCustomization<HelloUI>では、UIをカスタマイズするために必要な機能を定義しています。

using Dynamo.Controls;
using Dynamo.Wpf;

namespace DynamoUnchained.ExplicitNode
{
  public class HelloUINodeView : INodeViewCustomization<HelloUI>
  {
    public void CustomizeView(HelloUI model, NodeView nodeView)
    {
      var ui = new MyCustomControl();
      nodeView.inputGrid.Children.Add(ui);
      ui.DataContext = model;
    }

    public void Dispose()
    {
    }
  }
}

スライダーの変更

MyCustomControl.XAMLのスライダーを以下のように変更します。

<Slider 
  x:Name="slider" 
  Width="140" 
  IsSnapToTickEnabled="True" 
  TickFrequency="1" 
  Minimum="0" 
  Maximum="100" 
  Foreground="Black" 
  Margin="7" 
  Value="{Binding SliderValue}" />

その他

jsonの変更

初級編と同様にjsonファイルを作成し、"node_libraries"を以下のように書き換えます。

"node_libraries": [
    "DynamoUnchained.ExplicitNode, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "DynamoUnchained.ExplicitNode.Functions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
  ]

Dynamoパッケージフォルダーへのデプロイ

初級編と同様にデプロイの設定をします。

xcopy /Y "$(TargetDir)*.*" "$(AppData)\Dynamo\Dynamo Revit\2.3\packages\$(ProjectName)\bin\"
xcopy /Y "$(ProjectDir)pkg.json" "$(AppData)\Dynamo\Dynamo Revit\2.3\packages\$(ProjectName)"

参考

https://teocomi.com/dynamo-unchained-2-learn-how-to-develop-explicit-nodes-in-csharp/

https://alvpickmans.github.io/DynamoDevelopment-London-Hackathon-2019/

https://digitalidentity.co.jp/blog/creative/mvc-mvvm.html