> For the complete documentation index, see [llms.txt](https://mp4.gitbook.io/standard/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mp4.gitbook.io/standard/knowledge/bim/dev/razrabotka-plaginov/patterny-resheniya.md).

# Паттерны решения

В этом руководстве рассматриваются ключевые архитектурные подходы, которые обеспечивают **стабильность**, **масштабируемость** и **тестируемость** плагинов для Autodesk Revit.

## Command

**Паттерн Command (IExternalCommand)** — является обязательной «точкой входа» для любого плагина. Если задача плагина выполнить команду в один клик и без участия пользователя (скрипт), то архитектура плагина ограничивается только этим паттерном. Пример:

```csharp
[Transaction(TransactionMode.Manual)]
public class Main : IExternalCommand
{
    public Result Execute(ExternalCommandData commandData, 
         ref string message, ElementSet elements)
    {
        try
        {
            UIApplication uiapp = commandData.Application;
            Document doc = commandData.Application.ActiveUIDocument.Document;
            // Код плагина
            return Result.Succeeded;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
            return Result.Failed;
        }
    }
}
```

## MVVM

**Архитектурный паттерн MVVM** — применяется в дополнение к Command, когда плагину требуется графический интерфейс (WPF). MVVM представляет из себя архитектурный паттерн плагина, который состоит из трех независимых слоев: Main - View -ViewModel.

Применение данного подхода и строгое разделение логики от UI (пользовательского интерфейса) ставит за собой следующие основные цели:

1. **Масштабируемость и легкость поддержки.** Плагины для Revit имеют свойство разрастаться. В "спагетти-коде" (где логика перемешана с UI) добавление новой кнопки превращается в кошмар. В MVVM:

* Нужно изменить дизайн? Правится только **View**.
* Нужно изменить логику? Правится только **Model**.
* Нужно добавить валидацию введенных значений? Правите только **ViewModel**.

{% hint style="info" %}
Такая модульность позволяет нескольким разработчикам работать над одним плагином одновременно.
{% endhint %}

2. **Повторное использование кода (Reusability).** Логика, описанная в **Model**, становится независимой. Это позволяет использовать одни и те же классы обработки данных (например, расчет теплопотерь или специфицирование оборудования) в разных плагинах или даже в консольных утилитах, просто «пристегивая» к ним разные интерфейсы.
3. **Улучшение пользовательского опыта (UX).** Благодаря механизму **Bindings** и интерфейсу `INotifyPropertyChanged`, интерфейс плагина становится "живым". Пользователю не нужно нажимать кнопку «Обновить», чтобы увидеть изменения: данные в окне синхронизируются автоматически. Кроме того, паттерн позволяет легко реализовать валидацию — кнопка «Запустить» будет заблокирована на уровне ViewModel до тех пор, пока пользователь не введет корректные данные в поля View.
4. **Упрощение миграции между версиями Revit.** При переходе на новую версию Revit (например, с 2023 на 2025 с переходом на .NET 8) изменения часто затрагивают API. Благодаря MVVM, вам придется переписывать только слой **Model**, взаимодействующий с API, в то время как сложная визуальная часть (**View**) и логика поведения окна (**ViewModel**) останутся практически нетронутыми.

### IExternalCommand

Сбор данных из Revit (получение стен, объемов и параметров) происходит **вне паттерна MVVM** — в основном теле команды (классе, реализующем `IExternalCommand`). Именно здесь происходит обращение к `Document`, используется `FilteredElementCollector` и извлекаются нужные данные. В этом случае `IExternalCommand` только запускает окно и передает данные для работы, а дальнейшая логика взаимодействия данных и интерфейса строится по принципам MVVM.

```csharp
[Transaction(TransactionMode.Manual)]
public class MyCommand : IExternalCommand
{
    public Result Execute(ExternalCommandData commandData, 
          ref string message, ElementSet elements)
    {
        try
        {
           Document doc = commandData.Application.ActiveUIDocument.Document;
          // 1. 1 ВАРИАНТ - В случае маленькой логики получения данных 
          // Собираем все листы Revit
          List<ViewSheet> allSheets = new FilteredElementCollector(doc)
           .OfClass(typeof(ViewSheet))
           .WhereElementIsNotElementType()
           .Cast<ViewSheet>()
           .ToList();
          // Читаем общий ширф проекта из параметра информации о проекте "Номер проекта"
          Parameter projectCode = doc.ProjectInformation.LookupParameter("Номер проекта");
          string projectCodeValue = (projectCode != null && projectCode.HasValue) ? projectCode.AsString() : string.Empty;
          // Создаем Model
          List<SheetTemplate> sheetTemplates = allSheets
           .Select(s => new SheetTemplate(s, projectCodeValue))
           .ToList();
          // Проверка на пустоту
          if (sheetTemplates.Count == 0)
          {
           MessageBox.Show("Для печати необходим хотя бы один лист.");
           return Result.Cancelled;
          }
          // 1. 2 ВАРИАНТ - Если логика получения данных большая реализовываем класс сервиса для сбора
        
          // 2. Инициализируем ViewModel, передавая документ и список листов
          MainViewModel vm = new MainViewModel(doc, uiapp, visibleSheets, projectCodeValue);

          // 3. Создаем View
          MainView view = new MainView();
          // Привязываем контекст вида к видовой модели
          view.DataContext = vm;
          // Привязываем закрытие окна к ViewModel
          if (vm.CloseAction == null) vm.CloseAction = new Action(view.Close); 
          view.ShowDialog(); // Отображаем окно
          return Result.Succeeded;
        }
        catch (Exception ex)
        {
           MessageBox.Show(ex.Message);
           return Result.Failed;
        }
    }
}
```

{% hint style="warning" %}
Сбор исходных данных для дальнейшей работы внутри `WallViewModel`, нарушает принцип MVVM, т.к. **ViewModel** должна работать с "чистыми" данными (числами, строками) и при этом **Revit API** должен остаться на уровне **Модели** или в **Команде**, которая эту модель наполняет.
{% endhint %}

Где расположены методы Revit API?

* Если плагин только читает данные, то API во `ViewModel` вообще не нужен. Все исходные данные уже прочитаны в `Command` .
* Если плагин **меняет** Revit (создает стены, двигает окна), то во `ViewModel` появляются вспомогательные классы-сервисы.

### Model

* **Функция: "Что мы обрабатываем?".** Базовый слой, содержащий данные и бизнес-логику.
* **Состав:** Обертки над элементами Revit API (ViewSheet FamilyInstance), классы для хранения результатов расчетов или данные из внешних БД.
* **Золотое правило:** Модель абсолютно «слепа». Она не знает о существовании кнопок, окон и даже самой ViewModel. Её задача — просто хранить и предоставлять данные.
* **Пример:** Класс `SheetTemplate`, который хранит данные, полученные из элемента Revit.

```csharp
public class SheetTemplate : BaseViewModel
{

    // Поле и свойства выбора листа
    private bool _isSelected = true;
    public bool IsSelected
    {
        get => _isSelected;
        set { _isSelected = value; OnPropertyChanged(); }
    }
    // поле и его свойство - инкапсулированное поле
    private string _sheetName;
    public string SheetName
    {
        get => _sheetName;
        set { _sheetName = value; OnPropertyChanged(); }
    }
    // конструктор класса
    public SheetTemplate(ViewSheet sheet)
    {
        _sheetName = sheet.Name;
    }
}
```

### ViewModel

* **Функция: "Связующий узел".**
* **Состояние:** Хранит информацию о том, какой элемент выбран в списке, активны ли кнопки или другие элементы, заполнены ли обязательные поля, логику взаимодействия элементов WPF.
* **Реактивность:** Реализует интерфейс `BaseViewModel`. Как только значение во ViewModel меняется, View мгновенно перерисовывается.
* **Команды (RelayCommands):** Вместо классических обработчиков событий (Click), кнопки View привязаны к командам во ViewModel.

```csharp
public class MainViewModel : BaseViewModel
{
    // Делегат для закрытия окна
    public Action CloseAction { get; set; }
    // Документ Revit
    private readonly Document _doc;
    // Коллекция элементов
    public List<SheetTemplate> SheetTemplates { get; set; }
    private bool _isColorPrint;
    public bool IsColorPrint
    {
     get => _isColorPrint;
     set
     {
        if (SetProperty(ref _isColorPrint, value))
        {
            // Синхронизируем зависимые поля
            _isBlackAndWhite = !value;
            _isKeepSignColor = !value;
            OnPropertyChanged(nameof(IsBlackAndWhite));
            OnPropertyChanged(nameof(IsKeepSignColor));
            OnPropertyChanged(nameof(IsKeepSignColorEnabled));
        }
      }
    }
    private bool _isBlackAndWhite = true;
    public bool IsBlackAndWhite
    {
     get => _isBlackAndWhite;
     set
     {
        if (SetProperty(ref _isBlackAndWhite, value))
        {
            _isColorPrint = !value;
            _isKeepSignColor = value;
            OnPropertyChanged(nameof(IsColorPrint));
            OnPropertyChanged(nameof(IsKeepSignColor));
            OnPropertyChanged(nameof(IsKeepSignColorEnabled));
        }
      }
    }
    private bool _isKeepSignColor = true;
    public bool IsKeepSignColor
    {
     get => _isKeepSignColor;
     set => SetProperty(ref _isKeepSignColor, value);
    }
    public bool IsKeepSignColorEnabled => !IsColorPrint; // Отражает доступность чекбокса
    // Команда печати
    public RelayCommand PrintCommand { get; }
    // Конструктор видовой модели
    public MainViewModel(Document doc, List<SheetTemplate> sheetTemplates)
    {
      _doc = doc;
      SheetTemplates = sheetTemplates;
      // Метод CanPrint учитывает доступность нажатия
      PrintCommand = new RelayCommand(OnPrint, CanPrint); 
    }
    // Метод доступности
    private bool CanPrint() => SheetTemplates.Any(s => s.IsSelected);
    // Метод печати
    private void OnPrint()
    {
      // Метод печати, который описан в отдельном классе, например, PDFService
      // Метод закрытия окна после выполнения метода
      CloseAction?.Invoke();
    }
}
```

### View

* **Функция: "Как это выглядит?".** Это визуальная оболочка, созданная на WPF (XAML):
* **Состав:** Окна (Window), пользовательские элементы (UserControl), ресурсы стилей.
* **Золотое правило:** В файле `View.xaml.cs` (Code-behind) должен быть только вызов `InitializeComponent()`. Никаких расчетов и обращений к Revit API.
* **Связь:** Взаимодействует с данными только через **Bindings** (привязки). Это позволяет дизайнеру менять внешний вид плагина, не рискуя сломать код расчетов.

```xml
<DataGrid ItemsSource="{Binding SheetTemplates}">
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Печать">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <DataGridTextColumn Header="Имя листа" Binding="{Binding Name}"/>
    </DataGrid.Columns>
</DataGrid>
<CheckBox Content="Цветная печать" IsChecked="{Binding IsColorPrint}"/>
<CheckBox Content="Черно-белая печать" IsChecked="{Binding IsBlackAndWhite}"/>
<CheckBox Content="Сохранить цвет подписей" IsChecked="{Binding IsKeepSignColor}" IsEnabled="{Binding IsKeepSignColorEnabled}"/>
<Button Command="{Binding PrintCommand}" Content="Печать / Экспорт"/>
```

```csharp
public partial class MainView : Window 
{
    public MainView(MainViewModel vm) 
    {
        InitializeComponent();
    }
}
```

### RelayCommands

В архитектуре MVVM интерфейс `ICommand` (из пространства имен `System.Windows.Input`) играет ключевую роль. Он позволяет отделить логику, которая выполняется при нажатии кнопки, от самой кнопки в пользовательском интерфейсе.

**RelayCommand** — это пользовательская реализация интерфейса `ICommand`, которая служит мостом между **View** (кнопка в XAML) и **ViewModel** (метод C#). Она "транслирует" действие пользователя в вызов конкретного метода в логике.

Зачем нужен RelayCommand?

1. **Соблюдение MVVM:** Позволяет избежать размещения логики обработки событий (`Button_Click`) в файле `View.xaml.cs` (code-behind), сохраняя чистоту и разделение слоев.
2. **Управление состоянием:** Позволяет легко включать (`CanExecute`) или отключать (`CanExecuteChanged`) кнопки на основе текущего состояния ViewModel (например, если не заполнены обязательные поля, кнопка «ОК» будет неактивна).
3. **Тестируемость:** Поскольку логика выполнения команды находится в ViewModel, её легко протестировать без необходимости запускать пользовательский интерфейс.

Общая реализация класса:

```csharp
using System;
using System.Windows.Input; // Библиотека для работы с ICommand

/// <summary>
/// Реализация паттерна Команда (Command).
/// Позволяет передать логику из ViewModel во View (кнопки, пункты меню).
/// </summary>
public class RelayCommand : ICommand
{
    // Ссылка на метод, который выполнит основную работу (Execute)
    private readonly Action _execute;
    
    // Ссылка на метод-проверку, который возвращает true (кнопка активна) или false (заблокирована)
    private readonly Func<bool> _canExecute;

    /// <summary>
    /// Событие, на которое подписывается WPF, чтобы следить за состоянием кнопки.
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        // Передаем управление событием встроенному менеджеру WPF.
        // Он автоматически заставляет кнопку перепроверять состояние при кликах, табах и фокусе.
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    /// <summary>
    /// Конструктор команды
    /// </summary>
    /// <param name="execute">Метод, который выполнится при нажатии</param>
    /// <param name="canExecute">Метод-условие (необязательно)</param>
    public RelayCommand(Action execute, Func<bool> canExecute = null)
    {
        // Проверяем, что нам передали хотя бы действие (выполнять "ничего" нельзя)
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    /// <summary>
    /// Метод интерфейса ICommand. Вызывается самой WPF, чтобы понять, 
    /// должна ли кнопка быть серой (Disabled) или активной.
    /// </summary>
    /// <param name="parameter">Данные, приходящие из CommandParameter в XAML (обычно не используются)</param>
    public bool CanExecute(object parameter)
    {
        // Если метод-проверка не передан, значит кнопка всегда активна.
        // Если передан — вызываем его и возвращаем результат (true/false).
        return _canExecute == null || _canExecute();
    }

    /// <summary>
    /// Метод интерфейса ICommand. Вызывается WPF в момент нажатия на кнопку.
    /// </summary>
    public void Execute(object parameter)
    {
        // Просто запускаем тот метод, который мы сохранили в конструкторе
        _execute();
    }

    /// <summary>
    /// Метод для принудительного обновления интерфейса.
    /// Нужен, если данные изменились "втихую" в коде, и WPF не заметил, 
    /// что кнопку пора разблокировать.
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        // Команда системе WPF: "Брось всё и перепроверь доступность команд прямо сейчас"
        CommandManager.InvalidateRequerySuggested();
    }
}
```

### BaseViewModel

Класс, который создан для того, чтобы не дублировать реализацию интерфейса INotifyPropertyChanged и IDataErrorInfo. Классы модели или видовой модели являются наследниками данного класса.&#x20;

```csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

/// <summary>
/// Базовый класс для всех ViewModel и моделей с поддержкой уведомлений и валидации данных
/// </summary>
public abstract class BaseViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    #region УВЕДОМЛЕНИЕ О ИЗМЕНЕНИИ (INotifyPropertyChanged)

    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Метод для генерации события изменения свойства
    /// </summary>
    /// <param name="propertyName">Имя свойства (подставляется автоматически)</param>
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    /// Упрощенный метод для обновления значения свойства с проверкой на равенство
    /// </summary>
    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (Equals(field, value)) return false;

        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    #endregion

    #region ВАЛИДАЦИЯ (IDataErrorInfo)

    /// <summary>
    /// Сообщение об ошибке для всего объекта (не используется в WPF)
    /// </summary>
    public string Error => null;

    /// <summary>
    /// Индексатор для получения ошибки конкретного свойства
    /// </summary>
    public string this[string columnName] => ValidateProperty(columnName);

    /// <summary>
    /// Виртуальный метод для реализации логики проверки в дочерних классах.
    /// Переопределите его, чтобы описать правила валидации.
    /// </summary>
    /// <param name="columnName">Имя проверяемого свойства</param>
    /// <returns>Null если ошибок нет, иначе строка (например, пустая)</returns>
    protected virtual string ValidateProperty(string columnName) => null;

    /// <summary>
    /// Вспомогательный метод: возвращает null, если условие истинно, и пустую строку, если ложно.
    /// Используется для активации триггеров ошибок в XAML.
    /// </summary>
    protected string Check(bool condition) => condition ? null : string.Empty;

    #endregion
}

```

Пример реализации проверки значений свойств из интерфейса и доступности команды выглядит так:

```csharp
// Метод проверки свойств  из реализации класса BaseViewModel
protected override string ValidateProperty(string columnName)
{
    // Обычный switch, который точно поймет старый компилятор
    switch (columnName)
    {
        case nameof(WidthValue):
            return Check(CheckInputValue.ValidateStep10(WidthValue));
        case nameof(TopOffset):
            return Check(CheckInputValue.ValidateStep10(TopOffset));
        case nameof(BottomOffset):
            return Check(CheckInputValue.ValidateStep10(BottomOffset));
        case nameof(LengthValue):
            // Если круглый — ошибки нет (null), если прямоугольный — проверяем кратно 10
            bool isLengthValid = !IsRectangular || CheckInputValue.ValidateStep10(LengthValue);
            return Check(isLengthValid);
        default:
            return null; // Для всех остальных свойств (UserName и т.д.) ошибок нет
    }
}

// Метод проверки для кнопки "Разместить"
private bool CanPlace()
{
    // Проверяем только ключевые поля. Если ValidateProperty для всех вернул null — кнопка активна.
    string[] fields = { nameof(WidthValue), nameof(LengthValue), nameof(TopOffset), nameof(BottomOffset) };
    return fields.All(field => ValidateProperty(field) == null);
}
```

Для реализации данного механизма необходимо добавить в свойство Binding элемента разметки xaml свойство `ValidatesOnDataErrors=True`, например:

```csharp
Text="{Binding BottomOffset, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
```
