# NodeModel 案例研究 - 自定义 UI

相较于 Zero-Touch 节点，基于 NodeModel 的节点提供了更大的灵活性和更强大的功能。在本例中，我们将通过添加一个随机化矩形大小的集成滑块，来将 Zero-Touch 网格节点提升到下一个级别。

> 滑块会相对于单元大小缩放单元，因此用户不必为滑块提供正确的范围。

#### Model-View-Viewmodel 模式 <a href="#the-model-view-viewmodel-pattern" id="the-model-view-viewmodel-pattern"></a>

Dynamo 基于 [model-view-viewmodel](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) (MVVM) 软件体系结构模式，以使 UI 与后端保持分离。当创建 ZeroTouch 节点时，Dynamo 会在节点的数据与其 UI 之间绑定数据。要创建自定义 UI，我们必须添加数据绑定逻辑。

在较高级别上，在 Dynamo 中建立模型-视图关系有两个部分：

* `NodeModel` 类用于建立节点的核心逻辑（“模型”）
* `INodeViewCustomization` 类用于自定义如何查看 `NodeModel`（“视图”）

> NodeModel 对象已有关联的视图-模型 (NodeViewModel)，因此我们可以仅关注自定义 UI 的模型和视图。

#### 如何实现 NodeModel <a href="#how-to-implement-nodemodel" id="how-to-implement-nodemodel"></a>

NodeModel 节点与 Zero-Touch 节点有几个明显差异，我们将在本例中介绍这些差异。在我们进入 UI 自定义之前，让我们先构建 NodeModel 逻辑。

**1.创建项目结构：**

NodeModel 节点只能调用函数，因此我们需要将 NodeModel 和函数分离到不同的库中。对 Dynamo 软件包执行此操作的标准方法是为每个软件包创建单独的项目。先创建一个新的解决方案来包含项目。

> 1. 选择 `File > New > Project`
> 2. 选择 `Other Project Types` 以显示“解决方案”选项
> 3. 选择 `Blank Solution`
> 4. 将解决方案命名为 `CustomNodeModel`
> 5. 选择 `Ok`

在解决方案中创建两个 C# 类库项目：一个用于函数，一个用于实现 NodeModel 接口。

> 1. 在解决方案上单击鼠标右键，然后选择 `Add > New Project`
> 2. 选择类库
> 3. 将它命名为 `CustomNodeModel`
> 4. 单击 `Ok`
> 5. 重复该过程以添加另一个名为 `CustomNodeModelFunctions` 的项目

接下来，我们需要重命名自动创建的类库，然后将其添加到 `CustomNodeModel` 项目中。类 `GridNodeModel` 实现抽象的 NodeModel 类、`GridNodeView` 用于自定义视图，`GridFunction` 包含我们需要调用的任何函数。

> 1. 添加另一个类，方法是在 `CustomNodeModel` 项目上单击鼠标右键、选择 `Add > New Item...`，然后选择 `Class`。
> 2. 在 `CustomNodeModel` 项目中，我们需要 `GridNodeModel.cs` 和 `GridNodeView.cs` 类
> 3. 在 `CustomNodeModelFunction` 项目中，我们需要 `GridFunctions.cs` 类

在我们将任何代码添加到类之前，请为此项目添加必需的软件包。`CustomNodeModel` 将需要 ZeroTouchLibrary 和 WpfUILibrary，`CustomNodeModelFunction` 将仅需要 ZeroTouchLibrary。WpfUILibrary 将用于我们稍后进行操作的 UI 自定义，ZeroTouchLibrary 将用于创建几何图形。可以为项目单独添加软件包。由于这些软件包具有依存关系，因此将自动安装 Core 和 DynamoServices。

> 1. 在项目上单击鼠标右键，然后选择 `Manage NuGet Packages`
> 2. 仅安装该项目所需的软件包

Visual Studio 会将我们参照的 NuGet 软件包复制到构建目录中。这可以设置为 false，这样一来软件包中就不会存在任何不必要的文件。

> 1. 选择 Dynamo NuGet 软件包
> 2. 将 `Copy Local` 设置为 false

**2.继承 NodeModel 类**

如前所述，使 NodeModel 节点与 ZeroTouch 节点不同的主要方面是其对 `NodeModel` 类的实现。NodeModel 节点需要此类中的多个函数，我们可以通过在类名后添加 `:NodeModel` 来获取这些函数。

将以下代码复制到 `GridNodeModel.cs` 中。

```
using System;
using System.Collections.Generic;
using Dynamo.Graph.Nodes;
using CustomNodeModel.CustomNodeModelFunction;
using ProtoCore.AST.AssociativeAST;
using Autodesk.DesignScript.Geometry;

namespace CustomNodeModel.CustomNodeModel
{
    [NodeName("RectangularGrid")]
    [NodeDescription("An example NodeModel node that creates a rectangular grid. The slider randomly scales the cells.")]
    [NodeCategory("CustomNodeModel")]
    [InPortNames("xCount", "yCount")]
    [InPortTypes("double", "double")]
    [InPortDescriptions("Number of cells in the X direction", "Number of cells in the Y direction")]
    [OutPortNames("Rectangles")]
    [OutPortTypes("Autodesk.DesignScript.Geometry.Rectangle[]")]
    [OutPortDescriptions("A list of rectangles")]
    [IsDesignScriptCompatible]
    public class GridNodeModel : NodeModel
    {
        private double _sliderValue;
        public double SliderValue
        {
            get { return _sliderValue; }
            set
            {
                _sliderValue = value;
                RaisePropertyChanged("SliderValue");
                OnNodeModified(false);
            }
        }
        public GridNodeModel()
        {
            RegisterAllPorts();
        }
        public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
        {
            if (!HasConnectedInput(0) || !HasConnectedInput(1))
            {
                return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode()) };
            }
            var sliderValue = AstFactory.BuildDoubleNode(SliderValue);
            var functionCall =
              AstFactory.BuildFunctionCall(
                new Func<int, int, double, List<Rectangle>>(GridFunction.RectangularGrid),
                new List<AssociativeNode> { inputAstNodes[0], inputAstNodes[1], sliderValue });

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

这不同于 Zero-Touch 节点。让我们了解一下每个部分的作用。

* 指定节点属性，如名称、类别、InPort/OutPort 名称、InPort/OutPort 类型、描述。
* `public class GridNodeModel : NodeModel` 是一个从 `Dynamo.Graph.Nodes` 中继承 `NodeModel` 类的类。
* `public GridNodeModel() { RegisterAllPorts(); }` 是一个注册节点输入和输出的构造函数。
* `BuildOutputAst()` 返回 AST（抽象语法树），这是从 NodeModel 节点返回数据所需的结构。
* `AstFactory.BuildFunctionCall()` 调用 `GridFunctions.cs` 中的 RectangularGrid 函数。
* `new Func<int, int, double, List<Rectangle>>(GridFunction.RectangularGrid)` 指定函数及其参数。
* `new List<AssociativeNode> { inputAstNodes[0], inputAstNodes[1], sliderValue });` 将节点输入映射到函数参数。
* 如果输入端口未连接，则 `AstFactory.BuildNullNode()` 会构建空节点。这是为了避免在节点上显示警告。
* `RaisePropertyChanged("SliderValue")` 会在滑块值发生更改时通知 UI
* `var sliderValue = AstFactory.BuildDoubleNode(SliderValue)` 在 AST 中构建表示滑块值的节点
* 将输入更改为 functionCall 变量 `new List<AssociativeNode> { inputAstNodes[0], sliderValue });` 中的 `sliderValue` 变量

**3.调用函数**

`CustomNodeModelFunction` 项目将构建到 `CustomNodeModel` 中的单独程序集，以便可以调用它。

将以下代码复制到 `GridFunction.cs` 中。

```
using Autodesk.DesignScript.Geometry;
using Autodesk.DesignScript.Runtime;
using System;
using System.Collections.Generic;

namespace CustomNodeModel.CustomNodeModelFunction
{
    [IsVisibleInDynamoLibrary(false)]
    public class GridFunction
    {
        [IsVisibleInDynamoLibrary(false)]
        public static List<Rectangle> RectangularGrid(int xCount = 10, int yCount = 10, double rand = 1)
        {
            double x = 0;
            double y = 0;

            Point pt = null;
            Vector vec = null;
            Plane bP = null;

            Random rnd = new Random(2);

            var pList = new List<Rectangle>();
            for (int i = 0; i < xCount; i++)
            {
                y++;
                x = 0;
                for (int j = 0; j < yCount; j++)
                {
                    double rNum = rnd.NextDouble();
                    double scale = rNum * (1 - rand) + rand;
                    x++;
                    pt = Point.ByCoordinates(x, y);
                    vec = Vector.ZAxis();
                    bP = Plane.ByOriginNormal(pt, vec);
                    Rectangle rect = Rectangle.ByWidthLength(bP, scale, scale);
                    pList.Add(rect);
                }
            }
            pt.Dispose();
            vec.Dispose();
            bP.Dispose();
            return pList;
        }
    }
}
```

此函数类与 Zero-Touch 网格案例研究非常相似，但有一点不同：

* 由于已从 `CustomNodeModel` 调用函数，因此 `[IsVisibleInDynamoLibrary(false)]` 会阻止 Dynamo“看到”以下方法和类。

正如我们为 NuGet 软件包添加参照一样，`CustomNodeModel` 需要参照 `CustomNodeModelFunction` 才能调用函数。

> 在我们参照函数之前，CustomNodeModel 的 using 语句将处于不活动状态
>
> 1. 在 `CustomNodeModel` 上单击鼠标右键，然后选择 `Add > Reference`
> 2. 选择 `Projects > Solution`
> 3. 选中 `CustomNodeModelFunction`
> 4. 单击 `Ok`

**4.自定义视图**

要创建滑块，我们需要通过实现 `INodeViewCustomization` 接口来自定义 UI。

将以下代码复制到 `GridNodeView.cs` 中

```
using Dynamo.Controls;
using Dynamo.Wpf;

namespace CustomNodeModel.CustomNodeModel
{
    public class CustomNodeModelView : INodeViewCustomization<GridNodeModel>
    {
        public void CustomizeView(GridNodeModel model, NodeView nodeView)
        {
            var slider = new Slider();
            nodeView.inputGrid.Children.Add(slider);
            slider.DataContext = model;
        }

        public void Dispose()
        {
        }
    }
}
```

* `public class CustomNodeModelView : INodeViewCustomization<GridNodeModel>` 定义自定义 UI 所需的功能。

在完成设置项目结构后，使用 Visual Studio 的设计环境构建用户控件并在 `.xaml` 文件中定义其参数。通过工具箱，将滑块添加到 `<Grid>...</Grid>`。

> 1. 在 `CustomNodeModel` 上单击鼠标右键，然后选择 `Add > New Item`
> 2. 选择 `WPF`
> 3. 将用户控件命名为 `Slider`
> 4. 单击 `Add`

将以下代码复制到 `Slider.xaml` 中

```
<UserControl x:Class="CustomNodeModel.CustomNodeModel.Slider"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:CustomNodeModel.CustomNodeModel"
             mc:Ignorable="d" 
             d:DesignHeight="75" d:DesignWidth="100">
    <Grid Margin="10">
        <Slider Grid.Row="0" Width="80" Minimum="0" Maximum="1" IsSnapToTickEnabled="True" TickFrequency="0.01" Value="{Binding SliderValue}"/>
    </Grid>
</UserControl>
```

* 在 `.xaml` 文件中定义滑块控件的参数。*Minimum 和 Maximum* 属性定义此滑块的数值范围。
* 在 `<Grid>...</Grid>` 中，我们可以通过 Visual Studio 工具箱放置不同的用户控件

当创建 `Slider.xaml` 文件时，Visual Studio 会自动创建一个名为 `Slider.xaml.cs` 的 C# 文件，用于初始化滑块。更改此文件中的名称空间。

```
using System.Windows.Controls;

namespace CustomNodeModel.CustomNodeModel
{
    /// <summary>
    /// Interaction logic for Slider.xaml
    /// </summary>
    public partial class Slider : UserControl
    {
        public Slider()
        {
            InitializeComponent();
        }
    }
}
```

* 该名称空间应为 `CustomNodeModel.CustomNodeModel`

`GridNodeModel.cs` 定义滑块计算逻辑。

**5.配置为软件包**

在我们构建项目之前，最后一步是添加一个 `pkg.json` 文件，以便 Dynamo 可以读取软件包。

> 1. 在 `CustomNodeModel` 上单击鼠标右键，然后选择 `Add > New Item`
> 2. 选择 `Web`
> 3. 选择 `JSON File`
> 4. 将该文件命名为 `pkg.json`
> 5. 单击 `Add`

* 将以下代码复制到 `pkg.json` 中

```
{
  "license": "MIT",
  "file_hash": null,
  "name": "CustomNodeModel",
  "version": "1.0.0",
  "description": "Sample node",
  "group": "CustomNodes",
  "keywords": [ "grid", "random" ],
  "dependencies": [],
  "contents": "Sample node",
  "engine_version": "1.3.0",
  "engine": "dynamo",
  "engine_metadata": "",
  "site_url": "",
  "repository_url": "",
  "contains_binaries": true,
  "node_libraries": [
    "CustomNodeModel, Version=1.0.0, Culture=neutral, PublicKeyToken=null",
    "CustomNodeModelFunction, Version=1.0.0, Culture=neutral, PublicKeyToken=null"
  ]
}
```

* `"name":` 确定软件包的名称及其在 Dynamo 库中的分组
* `"keywords":` 提供用于搜索 Dynamo 库的搜索词
* `"node_libraries": []` 指示与软件包关联的库

  最后一步是构建解决方案并发布为 Dynamo 软件包。请参见“软件包展开”一章，以了解如何在联机发布之前创建本地软件包以及如何直接从 Visual Studio 构建软件包。

#### 常见问题： <a href="#common-issues" id="common-issues"></a>

1. 打开图形时，一些节点有多个同名端口，但图形在保存时看起来正常。此问题可能有几个原因。

常见的根本原因是，节点是使用重新创建端口的构造函数创建的。反之，应使用已载入端口的构造函数。这些构造函数通常标记为 `[JsonConstructor]` *参见下文以了解示例*

\![Broken JSON](https://github.com/DynamoDS/DynamoPrimerNew/blob/master-chs/.gitbook/assets/broken-json%20\(1\).jpg)

这可能是因为：

* 根本没有匹配的 `[JsonConstructor]`，或者未通过 JSON .dyn 给它传递 `Inports` 和 `Outports`。
* 有两个版本的 JSON.net 同时载入到同一进程中导致 .net 运行时失败，因此无法正确使用 `[JsonConstructor]` 属性来标记构造函数。
* 版本不同于当前 Dynamo 版本的 DynamoServices.dll 已与软件包捆绑在一起，并会导致 .net 运行时无法识别 `[MultiReturn]` 属性，因此标记有各种属性的 Zero Touch 节点将无法应用它们。您可能会发现，一个节点返回一个字典输出，而不是多个端口。

2. 在将存在一些错误的图形载入控制台后，节点会完全丢失。

* 如果反序列化因某种原因而失败，则可能会发生这种情况。最好仅序列化所需的特性。我们可以对不需要载入或保存的复杂特性使用 `[JsonIgnore]` 来忽略它们。诸如 `function pointer, delegate, action,` 或 `event` 之类的特性。由于这些特性通常无法反序列化并导致出现运行时错误，因此不应该对它们序列化。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://primer2.dynamobim.org/zh-cn/1_developer_primer_intro/3_developing_for_dynamo/5-nodemodel-case-study-custom-ui.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
