# Пример использования NodeModel — настраиваемый пользовательский интерфейс

Узлы на основе NodeModel обеспечивают гораздо больше гибкости и мощности, чем узлы Zero-Touch. В этом примере мы улучшим узел сетки 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), благодаря чему пользовательский интерфейс не зависит от серверной части. При создании узлов ZeroTouch в Dynamo выполняется привязка данных между данными узла и его пользовательским интерфейсом. Чтобы создать пользовательский интерфейс, необходимо добавить логику привязки данных.

В общих чертах, для установления взаимосвязи между моделью и видом в Dynamo используется два класса:

* Класс `NodeModel` для определения основной логики узла (модель).
* Класс `INodeViewCustomization` для настройки способа просмотра `NodeModel` (вид).

> У объектов NodeModel уже есть связанная пара модели и вида (NodeViewModel), поэтому мы можем сосредоточиться только на модели и виде для пользовательского интерфейса.

#### Реализация NodeModel <a href="#how-to-implement-nodemodel" id="how-to-implement-nodemodel"></a>

Узлы NodeModel имеют несколько существенных отличий от узлов Zero-Touch. Мы рассмотрим их на следующем примере. Прежде чем мы перейдем к настройке пользовательского интерфейса, создадим логику 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 будет использоваться в настройке адаптации пользовательского интерфейса, а ZeroTouchLibrary будет использоваться для создания геометрии. Пакеты можно добавлять для проектов по отдельности. Так как эти пакеты имеют зависимости, будут автоматически установлены Core и DynamoServices.

> 1. Щелкните проект правой кнопкой мыши и выберите `Manage NuGet Packages` (Управлять пакетами NuGet).
> 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 (входящего и выходящего портов), описания.
* `public class GridNodeModel : NodeModel` — это класс, наследующий класс `NodeModel` от класса `Dynamo.Graph.Nodes`.
* `public GridNodeModel() { RegisterAllPorts(); }` является конструктором, регистрирующим входные и выходные данные узла.
* `BuildOutputAst()` возвращает AST (абстрактное синтаксическое дерево) — необходимую структуру для возврата данных из узла NodeModel.
* `AstFactory.BuildFunctionCall()` вызывает функцию RectangularGrid из `GridFunctions.cs`.
* `new Func<int, int, double, List<Rectangle>>(GridFunction.RectangularGrid)` задает функцию и ее параметры.
* `new List<AssociativeNode> { inputAstNodes[0], inputAstNodes[1], sliderValue });` сопоставляет входные данные узла с параметрами функции.
* `AstFactory.BuildNullNode()` создает пустой узел, если входные порты не подключены. Это позволяет избежать отображения предупреждения на узле.
* `RaisePropertyChanged("SliderValue")` уведомляет пользовательский интерфейс об изменении значения ползунка.
* `var sliderValue = AstFactory.BuildDoubleNode(SliderValue)` создает узел AST, представляющий значение ползунка.
* Измените входные данные на переменную `sliderValue` в переменной functionCall `new List<AssociativeNode> { inputAstNodes[0], 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 Grid, но есть одно отличие:

* При указании `[IsVisibleInDynamoLibrary(false)]` Dynamo не видит следующий метод и класс, поскольку функция уже вызывается из `CustomNodeModel`.

Так же как мы добавили ссылки на пакеты NuGet, `CustomNodeModel` должен будет ссылаться на `CustomNodeModelFunction` для вызова функции.

> Оператор using для CustomNodeModel будет неактивен до тех пор, пока мы не сошлемся на функцию.
>
> 1. Щелкните правой кнопкой мыши `CustomNodeModel` и выберите `Add > Reference` (Добавить > Ссылка).
> 2. Выберите `Projects > Solution` (Проекты > Решение).
> 3. Выберите `CustomNodeModelFunction`.
> 4. Нажмите `Ok`

**4. Настройка вида.**

Чтобы создать ползунок, необходимо настроить пользовательский интерфейс, реализовав интерфейс `INodeViewCustomization`.

Скопируйте следующий код в `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>` определяет функции, необходимые для настройки пользовательского интерфейса.

После настройки структуры проекта используйте среду разработки 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`. Атрибуты *минимума и максимума* определяют числовой диапазон этого ползунка.
* Внутри `<Grid>...</Grid>` можно разместить различные пользовательские элементы управления с панели инструментов Visual Studio.

При создании файла `Slider.xaml` Visual Studio был автоматически создан файл C# с именем `Slider.xaml.cs`, который инициализирует ползунок. Измените пространство имен в этом файле.

```
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` (Файл JSON).
> 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]` *см. примеры ниже*.

\![Поврежденный файл JSON](https://github.com/DynamoDS/DynamoPrimerNew/blob/master-rus/.gitbook/assets/broken-json%20\(1\).jpg)

Это может произойти по следующим причинам.

* Соответствующий `[JsonConstructor]` просто отсутствует, или `Inports` и `Outports` не переданы из JSON DYN.
* В один и тот же процесс одновременно загружены две версии JSON.net, что привело к сбою в среде выполнения .NET, поэтому атрибут `[JsonConstructor]` невозможно корректно использовать для обозначения конструктора.
* Файл DynamoServices.dll, версия которого отличается от текущей версии Dynamo, включен в пакет и вызывает ошибку определения атрибута `[MultiReturn]` в среде выполнения .NET, поэтому невозможно применить необходимые атрибуты к узлам 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/ru/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.
