Конфигурации плагина¶
Для использования необходимо подключить nuget-пакет ModPlusAPI версии не ниже 44.0 (если в пакете модуля версия ниже)!
<PackageReference Include="ModPlusAPI" Version="44.0.0" ExcludeAssets="runtime" />
BaseConfigurationsContext и BaseConfigurationsContextWithoutOrgSettings устарели
В ModPlusAPI появились новые контексты ConfigurationsContext и ConfigurationsContextWithoutOrgSettings. Старые контексты помечены атрибутом [Obsolete] и оставлены только для совместимости: в них есть ошибки, которые нельзя исправить без изменения поведения уже написанных плагинов. В новых плагинах используйте только новые контексты, а существующие переводите по разделу Переход со старых контекстов.
Что это и зачем¶
Конфигурация — это именованный набор настроек плагина. Пользователь может создать несколько конфигураций (например, «Фасады», «Планы этажей», «Для заказчика А») и быстро переключаться между ними, не перенастраивая окно каждый раз заново.
В окне плагина это выглядит как выпадающий список конфигураций и ряд кнопок рядом с ним: создать, переименовать, дублировать, экспортировать, импортировать, удалить. Всё это уже реализовано в ModPlusAPI — разработчику остаётся описать, какие настройки входят в конфигурацию, и разместить готовый контрол в окне.
Систему конфигураций стоит использовать, если плагину нужно:
- хранить несколько наборов настроек и переключаться между ними;
- давать пользователю создавать, копировать, удалять и передавать коллегам (экспорт/импорт) эти наборы;
- запоминать, какой набор был выбран в прошлый раз;
- выглядеть единообразно с другими плагинами ModPlus.
Если у плагина один набор настроек без переключения, конфигурации не нужны — достаточно пользовательских настроек.
Из чего состоит¶
| Элемент | Что это | Кто пишет |
|---|---|---|
BaseConfiguration |
Базовый класс одной конфигурации: Id, Name, Description |
Разработчик наследует свой класс и добавляет в него настройки |
ConfigurationsContext<TConfiguration> |
Контекст (ViewModel) окна: загрузка, сохранение, переключение, команды для кнопок. Учитывает Настройки организации | Разработчик наследует свой контекст |
ConfigurationsContextWithoutOrgSettings<TConfiguration> |
То же самое, но без учёта Настроек организации | Разработчик наследует свой контекст |
ConfigurationsContextCore<TConfiguration> |
Общая основа двух контекстов выше. Напрямую наследоваться от неё нельзя | — |
ConfigurationsControl |
Готовый UI: заголовок, список конфигураций, кнопки | Просто размещается в XAML окна |
Пространства имён: ModPlusAPI.Models (BaseConfiguration), ModPlusAPI.Abstractions (ICloneable<T>), ModPlusAPI.Contexts (контексты), ModPlusAPI.Windows.Controls (ConfigurationsControl). Сборка: ModPlusAPI.
Какой контекст выбрать¶
Оба контекста имеют одинаковый набор свойств, событий, команд и методов, одинаковый порядок вызова методов и работают с одним и тем же ConfigurationsControl. Отличаются они только тем, откуда читаются и куда сохраняются конфигурации.
ConfigurationsContext |
ConfigurationsContextWithoutOrgSettings |
|
|---|---|---|
| Настройки организации | Учитываются | Не учитываются |
| Где хранятся конфигурации | В пользовательском файле или в файле организации (если он задан администратором) | Всегда в пользовательском файле |
Тип свойства StorageFile |
OrgSettingsService |
UserSettingsService |
| Блокировка файла на время работы | Да, для файла организации. Снимается после закрытия окна | Нет |
| Запрет редактирования | Возможен, если у пользователя нет прав на файл организации | Нет |
| Переключение между файлами конфигураций | Возможно, если администратор задал несколько файлов | Нет |
| Выбранная конфигурация запоминается | Отдельно для каждого файла | Одна на плагин |
Как выбрать:
- Конфигурации плагина имеет смысл централизованно раздавать сотрудникам организации (стандарты оформления, шаблоны, правила именования) →
ConfigurationsContext. - Конфигурации — личное дело каждого пользователя, и Настройки организации для этого плагина не нужны →
ConfigurationsContextWithoutOrgSettings.
Смена контекста не теряет данные пользователей
Если Настройки организации не заданы, ConfigurationsContext хранит конфигурации в том же файле и том же узле, что и ConfigurationsContextWithoutOrgSettings: <папка пользовательских настроек ModPlus>\UserData\<Имя плагина>.xml, узел Configurations. Те же файл и узел использовали и устаревшие контексты. Поэтому при переходе плагина с одного контекста на другой — в том числе со старых на новые — ранее созданные пользователем конфигурации сохраняются, миграция не нужна.
Быстрый старт¶
Ниже — минимальный рабочий пример из четырёх шагов. Пример использует ConfigurationsContext; для контекста без Настроек организации меняется только базовый класс в шаге 2.
Шаг 1. Класс конфигурации¶
Класс наследуется от BaseConfiguration, реализует ICloneable<T> и содержит настройки плагина в виде публичных свойств.
namespace MyPlugin.Models;
using ModPlusAPI.Abstractions;
using ModPlusAPI.Models;
/// <summary>
/// Конфигурация плагина
/// </summary>
public class MyConfiguration : BaseConfiguration, ICloneable<MyConfiguration>
{
private int _startNumber = 1;
private string _prefix;
/// <summary>
/// Начальный номер
/// </summary>
public int StartNumber
{
get => _startNumber;
set
{
if (_startNumber == value)
return;
_startNumber = value;
OnPropertyChanged();
}
}
/// <summary>
/// Префикс
/// </summary>
public string Prefix
{
get => _prefix;
set
{
if (_prefix == value)
return;
_prefix = value;
OnPropertyChanged();
}
}
/// <inheritdoc />
public MyConfiguration Clone() => new()
{
Id = Id,
Name = Name,
Description = Description,
StartNumber = StartNumber,
Prefix = Prefix
};
}
Требования к классу конфигурации:
- Публичный конструктор без параметров — он нужен и для создания новой конфигурации, и для XML-сериализации.
- Публичные свойства с
get; set;— сохраняются только они. Поля, свойства только для чтения и свойства с приватным сеттером в файл не попадут. OnPropertyChanged()в сеттерах — без этого UI не узнает об изменениях, а событие контекстаOnConfigurationPropertyChangedне сработает.Clone()копирует все настройки — метод используется кнопкой «Дублировать». Забытое вClone()свойство у копии получит значение по умолчанию.
BaseConfiguration уже содержит Id, Name и Description с уведомлениями об изменениях — объявлять их повторно не нужно.
Шаг 2. Контекст окна¶
Контекст наследуется от одного из базовых контекстов и обязан реализовать пять методов. Если никакой дополнительной логики не нужно, их тело может быть пустым. Когда и зачем вызывается каждый метод — в разделе Как это работает.
namespace MyPlugin.ViewModels;
using ModPlusAPI.Abstractions;
using ModPlusAPI.Contexts;
using ModPlusStyle.Controls;
using Models;
/// <summary>
/// Контекст главного окна
/// </summary>
public class MainContext : ConfigurationsContext<MyConfiguration>
{
/// <summary>
/// Initializes a new instance of the <see cref="MainContext"/> class.
/// </summary>
/// <param name="connector">Коннектор плагина</param>
/// <param name="parentWindow">Окно плагина</param>
public MainContext(IModPlusPluginGeneral connector, ModPlusWindow parentWindow)
: base(connector, parentWindow)
{
}
/// <inheritdoc />
public override void OnLoaded()
{
// Один раз при открытии окна, ДО загрузки конфигураций:
// подписка на внешние события, сбор данных документа
}
/// <inheritdoc />
public override void PrepareConfiguration(MyConfiguration configuration)
{
// Для каждой конфигурации, попадающей в список:
// миграция старого формата, заполнение данных из документа
}
/// <inheritdoc />
public override void OnConfigurationsLoaded()
{
// После каждой загрузки конфигураций, текущая уже выбрана:
// конфигурация по умолчанию, если список пуст; обновление UI
}
/// <inheritdoc />
public override void OnConfigurationsSaving()
{
// Перед каждым сохранением конфигураций:
// перенос в конфигурации состояния UI, не связанного привязками
}
/// <inheritdoc />
public override void OnClosed()
{
// Один раз при закрытии окна, ПОСЛЕ сохранения конфигураций:
// отписка от событий, освобождение ресурсов
}
}
Для контекста без Настроек организации меняется только базовый класс:
Шаг 3. Разметка окна¶
В окне нужно сделать три вещи:
- Привязать события окна к командам контекста.
Loaded→OnLoadedCommand(загрузка конфигураций),Closed→OnClosedCommand(сохранение). Сам контекст окно не отслеживает: без этих привязок конфигурации не загрузятся и не сохранятся. - Разместить
ConfigurationsControl. Он берётDataContextокна и сам привязывается к нужным свойствам и командам. - Привязать элементы настроек к
Configuration.<Свойство>— то есть к свойствам текущей выбранной конфигурации.
<mps:ModPlusWindow x:Class="MyPlugin.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:b="https://modplus.org/xaml/behaviors"
xmlns:mps="https://modplus.org/winfx/xaml/controls"
xmlns:apiControls="clr-namespace:ModPlusAPI.Windows.Controls;assembly=ModPlusAPI"
xmlns:apiConverters="clr-namespace:ModPlusAPI.Converters;assembly=ModPlusAPI">
<mps:ModPlusWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/ModPlusStyle;component/Styles/Controls.xaml" />
</ResourceDictionary.MergedDictionaries>
<XmlDataProvider x:Key="LangCommon" XPath="ModPlus/Common" />
<apiConverters:BooleanToVisibilityConverter x:Key="TrueToVisibleFalseToCollapsed"
FalseVisibility="Collapsed"
TrueVisibility="Visible" />
<apiConverters:BooleanToVisibilityConverter x:Key="TrueToCollapsedFalseToVisible"
FalseVisibility="Visible"
TrueVisibility="Collapsed" />
</ResourceDictionary>
</mps:ModPlusWindow.Resources>
<!-- 1. Загрузка и сохранение конфигураций -->
<b:Interaction.Triggers>
<b:EventTrigger EventName="Loaded">
<b:InvokeCommandAction Command="{Binding Path=OnLoadedCommand}" />
</b:EventTrigger>
<b:EventTrigger EventName="Closed">
<b:InvokeCommandAction Command="{Binding Path=OnClosedCommand}" />
</b:EventTrigger>
</b:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 2. Список конфигураций и кнопки управления -->
<apiControls:ConfigurationsControl Grid.Row="0"
Orientation="Horizontal"
ShowHeader="True" />
<!-- 3. Настройки текущей конфигурации: видны, только если конфигурация выбрана -->
<StackPanel Grid.Row="1"
Visibility="{Binding Path=IsConfigurationSelected, Converter={StaticResource TrueToVisibleFalseToCollapsed}}">
<TextBox Text="{Binding Path=Configuration.Prefix, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<mps:NumericBox Value="{Binding Path=Configuration.StartNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<!-- Подсказка, если конфигураций ещё нет -->
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{Binding Source={StaticResource LangCommon}, XPath=configurationCreateOrImport}"
Visibility="{Binding Path=IsConfigurationSelected, Converter={StaticResource TrueToCollapsedFalseToVisible}}" />
</Grid>
</mps:ModPlusWindow>
Первый запуск
При первом запуске у пользователя нет ни одной конфигурации, и Configuration равен null. Поэтому блок настроек скрывается по IsConfigurationSelected, а вместо него показывается подсказка «создайте или импортируйте конфигурацию» (строка configurationCreateOrImport из общих языковых ресурсов). Другой вариант — создать конфигурацию по умолчанию в OnConfigurationsLoaded(), см. пример ниже.
Шаг 4. Установка DataContext¶
namespace MyPlugin.Views;
using ViewModels;
/// <summary>
/// Главное окно плагина
/// </summary>
public partial class MainWindow
{
/// <summary>
/// Initializes a new instance of the <see cref="MainWindow"/> class.
/// </summary>
public MainWindow()
{
InitializeComponent();
DataContext = new MainContext(ModPlusConnector.Instance, this);
}
}
Готово: при открытии окна конфигурации загрузятся и выберется та, с которой пользователь работал в прошлый раз, а при закрытии всё сохранится автоматически.
Как это работает¶
Жизненный цикл контекста разделён на две независимые пары методов:
OnLoaded()/OnClosed()— жизнь окна. Вызываются ровно по одному разу: при открытии и при закрытии окна. Предназначены для подписки на внешние события (приложения, документа) и освобождения ресурсов.OnConfigurationsLoaded()/OnConfigurationsSaving()— жизнь набора конфигураций. Вызываются при каждой загрузке и каждом сохранении конфигураций, в том числе при смене пользователем файла конфигураций вConfigurationsContext.
Отдельно стоит PrepareConfiguration(configuration) — он вызывается для каждой конфигурации, попадающей в список.
Базовый класс сам выполняет только загрузку, выбор и сохранение конфигураций и сохранение имени выбранной конфигурации. Всё остальное — подписки, освобождение ресурсов, перенос состояния UI в конфигурации — делает плагин в своих реализациях этих методов.
Открытие окна¶
- Окно вызывает
OnLoadedCommand(событиеLoaded). - Вызывается ваш
OnLoaded(). Конфигурации ещё не загружены: списокConfigurationsпуст,Configurationравенnull. - Контекст читает конфигурации из хранилища, сортирует их по имени и добавляет в список. Для каждой вызывается ваш
PrepareConfiguration(). - Выбирается конфигурация, имя которой было сохранено в прошлый раз. Если такой нет — первая в списке. Если конфигураций нет — ничего не выбирается.
- Вызывается ваш
OnConfigurationsLoaded(). Если после него конфигурации есть, а выбранной нет (например, вы создали конфигурации, не выбрав ни одну), выбирается первая.
OnLoadedCommand выполняется один раз: повторное событие Loaded (например, при повторном включении окна в визуальное дерево) игнорируется, OnLoaded() гарантированно вызывается один раз, а конфигурации из источника заново не загружаются. Если данные, подготовленные в PrepareConfiguration(), зависят от документа, при смене документа плагин обновляет их сам.
Сохранять конфигурации явно через SaveConfigurations() можно уже в OnConfigurationsLoaded() — например, сразу после переноса старых настроек. Автоматическое сохранение при закрытии окна и смене файла включается только после успешного выполнения OnConfigurationsLoaded().
Закрытие окна¶
- Окно вызывает
OnClosedCommand(событиеClosed). - Вызывается ваш
OnConfigurationsSaving()— перенесите в конфигурации всё, что не связано с ними привязками. - Имя выбранной конфигурации сохраняется в личный файл
mpConfig.mpcfчерезUserConfigFileUtils. - Список конфигураций сохраняется в хранилище (
SaveConfigurations()). - Вызывается ваш
OnClosed(). Конфигурации к этому моменту уже сохранены: изменения, внесённые в них здесь, в файл не попадут. ConfigurationsContextосвобождаетStorageFile— снимается блокировка файла организации.
Каждый шаг выполняется отдельно: ошибка при сохранении не помешает вызову OnClosed() и освобождению файла. OnClosedCommand, как и OnLoadedCommand, выполняется один раз — повторные вызовы игнорируются.
Смена файла конфигураций¶
Только для ConfigurationsContext, если администратор задал несколько файлов конфигураций и пользователь переключил файл в открытом окне. Окно при этом остаётся открытым, поэтому OnLoaded() и OnClosed() не вызываются:
- Вызывается ваш
OnConfigurationsSaving()— для старого файла. Configurationсбрасывается вnull(срабатываетOnConfigurationChangingдля текущей конфигурации старого файла).- Конфигурации сохраняются в старый файл.
- Конфигурации загружаются из нового файла — так же, как в шагах 3–5 открытия окна:
PrepareConfiguration()для каждой, выбор сохранённой,OnConfigurationsLoaded().
Для ConfigurationsContextWithoutOrgSettings источник один и не меняется, поэтому OnConfigurationsLoaded() и OnConfigurationsSaving() вызываются ровно по одному разу — при открытии и закрытии окна. Контракт методов при этом тот же, поэтому плагин переводится с одного контекста на другой без изменения логики.
Ошибки¶
| Где произошла ошибка | Что происходит |
|---|---|
OnLoaded() |
Сообщение об ошибке, окно закрывается. Конфигурации не загружаются и не сохраняются, OnClosed() не вызывается |
Чтение конфигураций, PrepareConfiguration(), OnConfigurationsLoaded() |
Сообщение об ошибке, окно закрывается без сохранения конфигураций — файл не будет перезаписан неполными данными. OnClosed() вызывается, так как OnLoaded() выполнился успешно |
OnConfigurationsSaving(), сохранение |
Сообщение об ошибке. OnClosed() всё равно вызывается, файл организации освобождается |
SaveConfigurations() тоже защищён: до завершения загрузки списка (в том числе в OnLoaded() и PrepareConfiguration()) и после ошибки чтения конфигураций вызов ничего не делает — пустой или частично заполненный список не затрёт сохранённые настройки. Начиная с OnConfigurationsLoaded() метод работает.
Переключение конфигурации¶
Когда пользователь выбирает другую конфигурацию в списке:
- Срабатывает событие
OnConfigurationChanging— в аргументе предыдущая конфигурация. Если до этого ни одна конфигурация не была выбрана, событие не срабатывает. - Меняется свойство
Configuration. - Имя новой конфигурации сразу записывается в
mpConfig.mpcf— плагину делать это не нужно. - Срабатывает событие
OnConfigurationChanged— в аргументе новая конфигурация (может бытьnull).
Добавление конфигураций¶
PrepareConfiguration() вызывается для каждой конфигурации, попавшей в список любым способом: загрузка, создание, дублирование, импорт, а также добавление из кода в Configurations. Поэтому миграцию старого формата и заполнение данных, зависящих от документа, достаточно написать в одном месте.
PrepareConfiguration() вызывается из обработчика CollectionChanged, поэтому изменять коллекцию Configurations внутри него нельзя. Конфигурация в момент вызова уже находится в списке, но может быть ещё не выбрана.
Удаление конфигурации¶
После удаления текущей конфигурации выбирается предыдущая в списке. После удаления последней конфигурации Configuration становится null.
Импорт¶
Конфигурация добавляется в список только после успешного чтения файла. Если файл не читается или в нём нет конфигурации нужного типа, пользователь видит сообщение, а список не меняется.
Имена конфигураций¶
- Список всегда отсортирован по имени.
- Имена уникальны: при совпадении к имени добавляется номер —
Фасады (1),Фасады (2). - При дублировании и импорте конфигурация получает имя исходной с номером.
- Если создание без запроса имени (
askNameOnConfigurationCreation: false), новые конфигурации называются «Конфигурация 1», «Конфигурация 2» и т.д.
Выбранная конфигурация запоминается по имени. Имя сохраняется при выборе, при переименовании и ещё раз при закрытии окна (на случай, если имя изменили через привязку к Configuration.Name), поэтому после переименования конфигурация всё равно будет выбрана при следующем открытии.
Примеры¶
Подписка на события документа¶
Подписки делаются в OnLoaded(), а снимаются в OnClosed() — базовый класс этого не делает.
/// <inheritdoc />
public override void OnLoaded()
{
_application.DocumentChanged += OnDocumentChanged;
}
/// <inheritdoc />
public override void OnClosed()
{
_application.DocumentChanged -= OnDocumentChanged;
}
Конфигурация по умолчанию¶
/// <inheritdoc />
public override void OnConfigurationsLoaded()
{
if (!HasConfigurations)
AddNewConfiguration(string.Empty, string.Empty);
}
AddNewConfiguration() сам делает новую конфигурацию текущей. В ConfigurationsContext метод вызывается и после смены файла конфигураций, так что пустой файл тоже получит конфигурацию по умолчанию.
Перенос старых настроек¶
Если раньше плагин хранил настройки без конфигураций, их можно один раз перенести в конфигурацию в OnConfigurationsLoaded() и сразу сохранить, не дожидаясь закрытия окна:
/// <inheritdoc />
public override void OnConfigurationsLoaded()
{
if (HasConfigurations)
return;
AddNewConfiguration(string.Empty, string.Empty);
Configuration.Prefix = UserConfigFileUtils.GetValue("Prefix");
SaveConfigurations();
}
Состояние UI, не связанное привязками¶
Если часть состояния окна хранится в контексте (например, отмеченные элементы списка), а не в свойствах конфигурации, переносите его в конфигурацию в OnConfigurationsSaving() — и при закрытии окна, и перед сменой файла конфигураций. Если состояние должно переноситься и при переключении конфигураций, используйте событие OnConfigurationChanging: оно срабатывает также при смене файла, причём до сохранения.
/// <inheritdoc />
public override void OnConfigurationsSaving()
{
if (Configuration != null)
Configuration.SelectedCategories = SelectedCategories.Select(c => c.Id).ToList();
}
Получение конфигурации без показа окна¶
OnLoadedCommand можно выполнить вручную, не показывая окно, — например, чтобы получить выбранную пользователем конфигурацию и выполнить команду плагина с её настройками:
var context = new MainContext(ModPlusConnector.Instance, new MainWindow());
context.OnLoadedCommand.Execute(null);
var configuration = context.Configuration;
Окно не показывается, поэтому событие Closed не наступает: автоматического сохранения конфигураций и вызова OnClosed() не будет. Если они нужны (например, чтобы отписаться от событий, на которые подписался OnLoaded()), выполните OnClosedCommand вручную. Для ConfigurationsContext это же действие снимает блокировку файла организации — без него файл останется заблокирован.
Где хранятся данные¶
| Что | ConfigurationsContext |
ConfigurationsContextWithoutOrgSettings |
|---|---|---|
| Список конфигураций | Пользовательский файл UserData\<Имя плагина>.xml или файл организации, заданный администратором |
Всегда пользовательский файл UserData\<Имя плагина>.xml |
| Имя выбранной конфигурации | mpConfig.mpcf, отдельно для пользовательского файла и для каждого файла организации |
mpConfig.mpcf |
| Экспортированная конфигурация | Отдельный XML-файл, путь выбирает пользователь | То же |
Имя выбранной конфигурации всегда хранится в личном mpConfig.mpcf, даже при работе с файлом организации: этот файл может быть открыт только на чтение, а выбор у каждого пользователя свой.
В ConfigurationsContext выбор запоминается отдельно для каждого источника: при переключении между пользовательским файлом и файлами организации у каждого восстанавливается своя выбранная конфигурация. Для пользовательского файла используется тот же ключ, что и в устаревших контекстах, поэтому выбор пользователя при переходе на новый контекст сохраняется.
Подробнее о файлах пользовательских настроек — в статье Работа с пользовательскими настройками.
Справочник API¶
Конструктор контекста¶
Оба контекста имеют одинаковый конструктор:
protected ConfigurationsContext(
IModPlusPluginGeneral connector,
ModPlusWindow parentWindow,
bool configurationsWithDescription = false,
bool askNameOnConfigurationCreation = true)
| Параметр | Назначение |
|---|---|
connector |
Коннектор плагина. Его Name задаёт имя файла хранения и узла в mpConfig.mpcf |
parentWindow |
Окно плагина. Через него показываются диалоги ввода имени и сообщения |
configurationsWithDescription |
true — у конфигураций есть описание: оно показывается в выпадающем списке, а диалог создания/переименования запрашивает имя и описание |
askNameOnConfigurationCreation |
true — при создании конфигурации запрашивается имя. false — конфигурация создаётся сразу с автоматическим именем |
Свойства¶
| Свойство | Тип | Описание |
|---|---|---|
Configurations |
ObservableCollection<TConfiguration> |
Все конфигурации |
Configuration |
TConfiguration |
Текущая выбранная конфигурация, может быть null |
IsConfigurationSelected |
bool |
Выбрана ли конфигурация. Удобно для Visibility/IsEnabled |
HasConfigurations |
bool |
Есть ли хотя бы одна конфигурация |
ConfigurationsWithDescription |
bool |
Значение одноимённого параметра конструктора |
ParentWindow |
ModPlusWindow |
Окно плагина |
UserConfigFileUtils |
UserConfigFileUtils |
Доступ к личному файлу mpConfig.mpcf в узле плагина |
StorageFile |
OrgSettingsService / UserSettingsService |
Сервис файла, в котором хранятся конфигурации (тип зависит от контекста) |
UserConfigFileUtils и StorageFile можно использовать не только для конфигураций, но и для любых других настроек плагина:
UserConfigFileUtils— для простых значений, которые не входят в конфигурацию: состояние окна, последний выбранный элемент и т.п.StorageFile— для дополнительных данных, которые должны храниться рядом с конфигурациями. ВConfigurationsContextэтоOrgSettingsService: методыSave(data, name)/Load<T>(name), данные подчиняются Настройкам организации. ВConfigurationsContextWithoutOrgSettingsэтоUserSettingsService: методыSet(data, name)/Get<T>(name), данные всегда в пользовательском файле.
Не используйте в StorageFile имя узла Configurations — под этим именем хранится сам список конфигураций. В ConfigurationsContext не обращайтесь к StorageFile в OnClosed(): сразу после него файл освобождается, и данные, которые должны попасть в файл организации, сохраняйте в OnConfigurationsSaving().
События¶
| Событие | Когда срабатывает |
|---|---|
OnConfigurationChanging |
Перед сменой выбранной конфигурации. Аргумент — предыдущая конфигурация. Не срабатывает, если текущей конфигурации не было. При смене файла конфигураций срабатывает до сохранения, поэтому изменения, внесённые в обработчике, будут сохранены |
OnConfigurationChanged |
После смены выбранной конфигурации (после PropertyChanged для Configuration). Аргумент — новая конфигурация, может быть null |
OnConfigurationPropertyChanged |
При изменении любого свойства текущей конфигурации. sender — конфигурация, e.PropertyName — имя свойства |
Абстрактные методы¶
| Метод | Когда вызывается | Для чего |
|---|---|---|
OnLoaded() |
Один раз при открытии окна, до загрузки конфигураций | Подписка на внешние события, сбор данных, нужных в PrepareConfiguration() |
PrepareConfiguration(TConfiguration) |
Для каждой конфигурации, добавленной в список: загрузка, создание, дублирование, импорт | Миграция старого формата, заполнение данных, зависящих от документа. Изменять Configurations здесь нельзя |
OnConfigurationsLoaded() |
После каждой загрузки конфигураций: при открытии окна и после смены файла. Текущая конфигурация уже выбрана | Конфигурация по умолчанию для пустого списка, перенос старых настроек (с явным SaveConfigurations()), обновление UI, зависящего от набора конфигураций |
OnConfigurationsSaving() |
Перед каждым сохранением: при закрытии окна и перед сменой файла. Не вызывается, если загрузка завершилась ошибкой | Перенос в конфигурации состояния UI, не связанного привязками |
OnClosed() |
Один раз при закрытии окна, после сохранения конфигураций. Только если OnLoaded() выполнился без ошибок |
Отписка от событий, освобождение ресурсов, захваченных в OnLoaded() |
Публичные методы¶
| Метод | Описание |
|---|---|
AddNewConfiguration(string baseName, string description) |
Добавляет конфигурацию, делает её текущей и сортирует список. Пустое baseName — автоматическое имя «Конфигурация N» |
RenameConfiguration(string baseName, string description) |
Переименовывает текущую конфигурацию с учётом уникальности имени и обновляет сохранённый выбор |
SaveConfigurations() |
Сохраняет список конфигураций немедленно, не дожидаясь закрытия окна. Работает начиная с OnConfigurationsLoaded(); раньше (в OnLoaded(), PrepareConfiguration()) и после ошибки чтения конфигураций ничего не делает |
Команды¶
Команды уже привязаны в ConfigurationsControl, но их можно использовать и в собственной разметке — например, если нужен нестандартный UI.
| Команда | Действие |
|---|---|
OnLoadedCommand |
Загрузка конфигураций — привязывается к Loaded окна. Выполняется один раз, можно вызвать вручную без показа окна |
OnClosedCommand |
Сохранение конфигураций — привязывается к Closed окна. Выполняется один раз |
AddNewConfigurationCommand |
Создать конфигурацию |
RenameConfigurationCommand |
Переименовать текущую |
CloneConfigurationCommand |
Дублировать текущую |
ExportCurrentConfigurationCommand |
Экспортировать текущую в XML-файл. Недоступна, если конфигурация не выбрана |
ImportConfigurationCommand |
Импортировать конфигурацию из XML-файла. Файл без подходящих данных отклоняется с сообщением |
RemoveCurrentConfigurationCommand |
Удалить текущую |
ConfigurationsControl¶
| Свойство | Значение по умолчанию | Описание |
|---|---|---|
Orientation |
Horizontal |
Horizontal — заголовок, список и кнопки в одну строку. Vertical — друг под другом, ширина контрола фиксирована (компактный вариант для боковых панелей) |
ShowHeader |
True |
Показывать заголовок «Конфигурация:» |
При работе с ConfigurationsContext кнопки создания, переименования, дублирования, импорта и удаления автоматически блокируются, если у пользователя нет прав на редактирование файла организации.
Переход со старых контекстов¶
Старые контексты остались в ModPlusAPI и продолжают работать, но при сборке компилятор выдаёт предупреждение CS0618 с указанием, на что заменить класс. Причины, по которым они заменены:
BaseConfigurationsContext—OnLoaded()иOnClosed()вызывались не только при открытии и закрытии окна, но и при каждой смене файла конфигураций. Подписки, сделанные вOnLoaded(), дублировались, а ресурсы, освобождённые вOnClosed(), пропадали при открытом окне.BaseConfigurationsContextWithoutOrgSettings— не было обработки конфигураций, добавляемых при создании, дублировании и импорте, а после удаления последней конфигурации выбранной оставалась удалённая.
Данные пользователей переносить не нужно: новые контексты читают те же файлы и узлы.
Порядок перехода¶
- Обновите ModPlusAPI до 44.0.
-
Замените базовый класс:
Было Стало BaseConfigurationsContext<T>ConfigurationsContext<T>BaseConfigurationsContextWithoutOrgSettings<T>ConfigurationsContextWithoutOrgSettings<T> -
Разнесите код старых
OnLoaded()иOnClosed()по новым методам. Главное отличие: в старом контекстеOnLoaded()вызывался после загрузки конфигураций, аOnClosed()— до сохранения. В новом — наоборот.Код в старом контексте Куда перенести OnLoaded(): подписки на события, сбор данных документаOnLoaded()OnLoaded(): обработка каждой загруженной конфигурации (миграция, заполнение данных)PrepareConfiguration()OnLoaded(): работа сConfigurations/Configuration(конфигурация по умолчанию, обновление UI)OnConfigurationsLoaded()OnClosed(): запись данных в конфигурации,StorageFile.Save(...)/Set(...)OnConfigurationsSaving()OnClosed(): отписка от событий, освобождение ресурсовOnClosed() -
Удалите из кода плагина ручную запись имени выбранной конфигурации в
UserConfigFileUtils, если она была, — новый контекст делает это сам. - Проверьте сценарии: первый запуск без конфигураций, создание, дублирование, импорт, удаление последней конфигурации, закрытие и повторное открытие окна. Для
ConfigurationsContext— ещё и переключение файла конфигураций, если для плагина заданы Настройки организации.
Код в OnLoaded(), обращающийся к Configuration
Это самая частая ошибка при переходе. В новом OnLoaded() конфигурации ещё не загружены, Configuration равен null, и код, перенесённый без изменений, либо упадёт с NullReferenceException, либо молча ничего не сделает. Такой код должен быть в OnConfigurationsLoaded().
Частые ошибки¶
Конфигурации не загружаются или не сохраняются.
Не привязаны события окна Loaded/Closed к OnLoadedCommand/OnClosedCommand (шаг 3).
Изменения в полях не отражаются или не сохраняются.
В сеттерах свойств конфигурации нет OnPropertyChanged(), либо свойство не публичное или без публичного сеттера.
Изменения, внесённые в OnClosed(), не сохраняются.
OnClosed() вызывается после сохранения. Переносите данные в конфигурации в OnConfigurationsSaving().
В OnLoaded() Configuration равен null.
Так и должно быть: OnLoaded() вызывается до загрузки конфигураций. Код, работающий с конфигурациями, перенесите в OnConfigurationsLoaded().
У дубликата часть настроек сброшена.
Новое свойство не добавлено в Clone().
Сборка падает с ошибкой «тип ConfigurationsContext не найден».
В проекте подключён ModPlusAPI ниже 44.0 — обновите версию пакета.
Предупреждение CS0618 о BaseConfigurationsContext или BaseConfigurationsContextWithoutOrgSettings.
Используется устаревший контекст — переведите плагин на новый по разделу Переход со старых контекстов.
Файл с конфигурациями вдруг стал общим для всех пользователей.
Для плагина заданы Настройки организации, а используется ConfigurationsContext. Если плагину это не нужно — используйте ConfigurationsContextWithoutOrgSettings.