DLL с UserControl на Avalonia C#
Пишу приложение с открытой архитектурой. Плагины с заданным интерфейсом в виде DLL находятся в отдельной папке и при старте приложения загружаются и регистрируются в контейнере DI. Обычная DLL с функциями подключается и работает. Но заказчику нужно, чтобы каждый плагин имел свою View-шку с произвольным набором кнопок, селекторов, полей ввода и т.п. Подскажите, как создать такую DLL-ку и вывести в основном окне приложения UserControl из этой DLL. За рабочий пример буду очень признателен.
Ответы (1 шт):
Покажу простой пример на WPF, под Avalonia адаптируете самостоятельно.
Создам основной WPF проект, назову его
ShellВ этот проект я добавлю NuGet:
CommunityToolkit.MVVM, чтобы не писать команды и INPC ручками. И также добавлюMicrosoft.Extensions.DependencyInjection(DI контейнер).Далее добавлю стандартный класс
MainViewModel(VM для основного окна).Удалю из
App.xamlстроку запуска (StartupUri), ибо окно будем запускать вручную, через DI.В
App.xaml.csпереопределюOnStartup, инициализировав там контейнер и запуск приложения.protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var services = new ServiceCollection(); services.AddSingleton<MainWindow>(); services.AddSingleton<MainViewModel>(); var serviceProvider = services.BuildServiceProvider(); var mainWindow = serviceProvider.GetRequiredService<MainWindow>(); mainWindow.DataContext = serviceProvider.GetRequiredService<MainViewModel>(); mainWindow.Show(); }Запускаем, должно появиться пустое окно. Поздравляю, с подготовкой основного приложения закончили.
Теперь нам нужен другой проект, который будет содержать в себе все интерфейсы (контракты) между всеми проектами. У меня решение зовется
SomeApp, назову проект с контактамиSomeApp.Contracts. Это простая библиотека классов.В этом проекте переименовываем стартовый
Class1вIHostApiи меняем его наinterface. Это будет интерфейс, через который плагин будет иметь нужные механизмы (отправить лог, записать в файл, вывести сообщение, перейти на страницу, и т.д.). Для примера я пропишу вывод сообщения.public interface IHostApi { void ShowMessage(string text); }Теперь создадим еще один интерфейс,
IPlugin. В нем мы пропишем все то, чего плагин должен реализовать. Если так подумать, то в плагине обычно есть имя, описание. Также он должен вернуть объект View для отображения. Собственно это и просим в интерфейсе.public interface IPlugin { string Name { get; } string Description { get; } object CreateView(IHostApi hostApi); }
Вернемся теперь к основному проекту, а именно в
App.xaml.cs. Наша цель - загружать .dll плагинов из указанной папки и, как пример, регистрировать их в контейнере.В зависимости проекта добавим проект
Contracts.Далее создадим метод загрузки плагинов:
private List<IPlugin> LoadPluginInstances(string folder) { var result = new List<IPlugin>(); if (!Directory.Exists(folder)) return result; foreach (var dll in Directory.GetFiles(folder, "*.dll")) { try { var asm = Assembly.LoadFrom(dll); var pluginTypes = asm.GetTypes() .Where(type => typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface); foreach (var pluginType in pluginTypes) { if (Activator.CreateInstance(pluginType) is IPlugin pluginInstance) result.Add(pluginInstance); } } catch (Exception ex) { Console.WriteLine($"Ошибка загрузки {dll}: {ex.Message}"); } } return result; }Теперь допишем в
OnStartupрегистрацию этих плагинов:var services = new ServiceCollection(); var plugins = LoadPluginInstances("Plugins"); foreach (var plugin in plugins) { services.AddSingleton(plugin); }Обратите внимание, что плагины я сразу инициализирую, после чего они висят как синглтоны в контейнере. Это не очень оптимально, лучше дать контейнеру самому выбирать когда и что загружать. Также стоит подумать, нужны ли плагины в контейнере, может лучше сделать отдельный сервис, который будет их хранить, загружать, управлять? Решение этих вопросов оставлю вам...
Переходим к
MainViewModel. Давайте в ней получим все зарегистрированные плагины, выведем на экран список плагинов, кнопками, а по клику будем отображать View конкретного плагина. Получаем в итоге такое:public partial class MainViewModel(IEnumerable<IPlugin> plugins, IHostApi hostApi) : ObservableObject { [ObservableProperty] private List<IPlugin> _plugins = [.. plugins]; [ObservableProperty] private object? _currentView; [RelayCommand] public void OpenPlugin(IPlugin plugin) { CurrentView = plugin.CreateView(hostApi); } }В
IEnumerable<IPlugin>контейнер подставит все зарегистрированные в нем объекты с типомIPlugin.Пропишем теперь простой вид окна:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ContentControl Grid.Row="0" Content="{Binding CurrentView}" /> <ItemsControl Grid.Row="1" ItemsSource="{Binding Plugins}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Button Content="{Binding Name}" Command="{Binding DataContext.OpenPluginCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ItemsControl}}" CommandParameter="{Binding}" Margin="5"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid>Ну и последнее, что нам осталось сделать, так это реализовать и зарегистрировать
IHostApi. Я создам просто класс, в котором пропишу следующее:public class HostApi : IHostApi { public void ShowMessage(string text) { MessageBox.Show(text); } }А в контейнер зарегистрирую так:
services.AddTransient<IHostApi, HostApi>();.
Теперь сам плагин. Делаем очередной проект, но на этот раз не простая библиотека классов, а "Библиотека классов WPF" (у вас авалония, там найдете аналогичное). Это нужно для того, чтобы мы могли использовать пользовательские элементы управления (User Control). Мой проект будет называться
SomeApp.Plugin.Calculator.В этом проекте создаем View (User Control) с нужным видом. У меня будет просто
TextBlockс большим текстом, ничего более.Далее создаем класс, который реализует
IPlugin(название любое, у меняPlugin). Наследуем класс от интерфейса и реализуем все то, что он требует. ВCreateViewмы должны вернуть View плагина.public class Plugin : IPlugin { public string Name => "Калькулятор"; public string Description => "Простой калькулятор в плагине."; public object CreateView(IHostApi hostApi) { hostApi.ShowMessage("Сообщение через HostApi."); return new View(); } }Собираем, закидываем эту .dll в папку
Pluginsрядом с основным проектом и запускаем.
Как видите, есть одна кнопка с названием плагина. По нажатию на кнопку страница из плагина загрузилась и отобразилась. Также есть сообщение, которое мы отправили через HostApi. Собственно, вот и все. Самая простая реализация плагинов готова, остальное уже тонкие реализации под конкретный проект.
На что стоит обратить внимание:
- Управление плагинами стоит отдать отдельному сервису, а не грузить их вот так нагло в контейнер.
- Плагины делать синглтонами не лучшая идея. Но тут зависит от задач.
- Метод
CreateViewсейчас возвращаетobject. Лучше сделать дополнительный интерфейс, который ограничит возвращаемый тип. - Внутри плагина вы можете также делать VM слои, просто укажите
DataContextView слою при его создании. - Вы можете пробросить DI в плагин. Для этого дополните
IPluginдополнительным методом, через который плагин сможет регистрировать свои типы в контейнере (включая V и VM), а вCreateViewсоздавайте не сами, а через контейнер (ActivatorUtilities). Ну и в зависимости не весь пакет контейнера, аMicrosoft.Extensions.DependencyInjection.Abstractions. НО! Помните про безопасность. Если плагины пишете не вы, то стоит задуматься, что люди могут "дергать" в вашем приложении (по этой причине в примере используетсяIHostApi). - Если вы в плагин добавили сторонний пакет, то его нужно также добавить и основному проекту, помните про это.
