Получение всех команд NET, часть 2
Продолжение части 1. Попробую поиграться с окнами и "прокидыванием" данных в них. Окна - на WPF (ну не люблю я WinForms, плюс MVC / MVP мне как-то не зашли).
Общая идея состоит в том, что окно само по себе ничего не делает - только отображает какие-то данные и посылает "команды" какому-то стороннему модулю. А уже этот модуль там чего-то с данными творит, модифицирует (при необходимости), записывает в базу и теде, и тепе.
И вот теперь я на распутье: по хорошему надо делать 2 разные сборки - одна будет заведовать окнами, вторая - ViewModel'ями. С другой стороны, нередки ситуации (по крайней мере в моей практике такое встречается), когда одно окно вызывает второе, второе - третье, и по закрытию третьего какие-то данные надо "пробрасывать" в первое. Я не настолько крут в C#, чтоб это все реализовать, так что будет отдельная сборка KpblcCadInfrastructure.Core.NET (NET6), где будут и View, и ViewModel. Хотя, в этой сборке будет два подкаталога - Views и ViewModels, так что при необходимости "раскидать" по разным сборкам особого труда не должно составить
Как бы мне ни хотелось сразу упасть в логику, но придется прописать пару окон. Одно будет показывать перечень загруженных сборок, второе - перечень объявленных команд. В темы оформления углубляться не буду, если интересно - могу потом попробовать пораспинываться на эту тему.
Если посмотреть на окна, видно, что у обоих есть флажок "показать только внешние сборки". Имеется в виду, что это сборки, которые подгружаются не из %ProgramFiles%. При этом получение команд зависит от перечня сборок. Как-то начинает свербеть чуть пониже спины, что такое получение "несистемных" сборок стоит прописывать только один раз. Так что дорабатываю интерфейс IAssemblyRepository и его реализацию
Теперь можно и с ViewModel начинать ковыряться
Для начала в KpblcCadInfrastructure.Abstractions прописываю абстрактный класс ViewModel, от которого буду наследовать все ViewModel'и:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | using System.ComponentModel; using System.Runtime.CompilerServices; namespace KpblcCadInfrastructure.Abstractions.ViewModels.Base { public abstract class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public virtual void OnPropertyChanged(string PropertyName) { if (PropertyChanged != null) { PropertyChanged.Invoke(this, new PropertyChangedEventArgs(PropertyName)); } } public virtual bool Set<T>(ref T Field, T value, [CallerMemberName] string PropertyName = null) { if (Equals(Field, value)) { return false; } Field = value; OnPropertyChanged(PropertyName); return true; } public virtual string Title { get => _Title; set => Set(ref _Title, value); } private string _Title; } } |
Это изначальная ViewModel, которая тупо позволяет быстро и просто, с использованием метода Set, не только что-то чему-то назначить, но еще и про это проорать на всю Ивановскую. Поле Title понадобится для назначения заголовков окон.
Ну ок, теперь ползу в KpblcCadInfrastructure.Core, и там прописываю ViewModel для сборок (начну с них, поскольку тут будет проще слеганца). Для начала вспоминаю, что окно вызывается уже самим nanoCAD, так что остаются только такие задачи:
- В окне вывести все сборки - полное имя, версия
- Дать возможность закрыть окно
При этом закрытие окна в текущих реалиях можно вообще не заморачиваться, соответствующий код можно даже не писать. Назначение кнопке атрибут IsCancel="True" уже автоматом закроет окно при нажатии на кнопку или нажатии Esc. Так что задача остается только одна - показать информацию о сборках.
Так, что-то я увлекся и забыл про git...
[/su_spoiler]
Теперь пора прописывать ViewModel для получения сборок. Сначала поиграться - типа "Нажали на галочку, вывалилось сообщение":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using KpblcCadInfrastructure.Abstractions.ViewModels.Base; namespace KpblcCadInfrastructure.Core.NET.ViewModels { public class AssembliesViewModel : ViewModel { public AssembliesViewModel() { Title = "Показать сборки"; } public bool ShowCustomAssemblies { get => _showCustomAssemblies; set { if (Set(ref _showCustomAssemblies, value)) { // ???????? } } } private bool _showCustomAssemblies; } } |
И приводим в соответствие xaml-описание окна AssembliesWindow (прописывать буду по шагам, так что потерпите):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <Window x:Class="KpblcCadInfrastructure.Core.NET.Views.Windows.AssembliesWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:KpblcCadInfrastructure.Core.NET.Views.Windows" xmlns:vm="clr-namespace:KpblcCadInfrastructure.Core.NET.ViewModels" mc:Ignorable="d" Title="AssembliesWindow" Height="250" Width="600" d:DataContext="{d:DesignInstance vm:AssembliesViewModel}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <CheckBox Grid.Row="0" Content="Показывать только внешние сборки" /> <DataGrid Grid.Row="1"></DataGrid> <UniformGrid Grid.Row="2" Rows="1" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5"> <Button Content="Закрыть" IsCancel="True"></Button> </UniformGrid> </Grid> </Window> |
Добавляются две строки в объявлении окна:
1 2 | xmlns:vm="clr-namespace:KpblcCadInfrastructure.Core.ViewModels" d:DataContext="{d:DesignInstance vm:AssembliesViewModel}" |
Первая сообщает, что есть такое типа пространство имен, с заголовком vm, а вторая говорит, что для разработки надо взять определенный класс из vm, откуда уже "подсасывать" показываемые данные.
Теперь связываю Title и CheckBox с соответствующими полями ASsemblyViewModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <Window x:Class="KpblcCadInfrastructure.Core.NET.Views.Windows.AssembliesWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:KpblcCadInfrastructure.Core.NET.Views.Windows" xmlns:vm="clr-namespace:KpblcCadInfrastructure.Core.NET.ViewModels" mc:Ignorable="d" Title="{Binding Title}" Height="250" Width="600" d:DataContext="{d:DesignInstance vm:AssembliesViewModel}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <CheckBox Grid.Row="0" Content="Показывать только внешние сборки" IsChecked="{Binding ShowCustomAssemblies}"/> <DataGrid Grid.Row="1"></DataGrid> <UniformGrid Grid.Row="2" Rows="1" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5"> <Button Content="Закрыть" IsCancel="True"></Button> </UniformGrid> </Grid> </Window> |
Ну и надо не забыть в вызывающей команде связать ViewModel и окно:
1 2 3 4 5 6 7 8 9 10 |
Теперь, если запустить проект, и поставить точку останова на строке в ASsembliesViewModel if (Set(ref _showCustomAssemblies, value)), можно будет увидеть, что при клике на галочке выполнение как раз на этой строке и останавливается. Правда, ничего не происходит - но сейчас подправлю.
Чего я там хотел? Чтоб сообщение вываливалось? Ну так для этого есть сервис сообщений! Осталось его передать в ViewModel (ну и по ходу дела переделать вызов ViewModel):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using KpblcCadInfrastructure.Abstractions.Interfaces; using KpblcCadInfrastructure.Abstractions.ViewModels.Base; namespace KpblcCadInfrastructure.Core.NET.ViewModels { public class AssembliesViewModel : ViewModel { public AssembliesViewModel(IMessageService MessageService) { _messageService = MessageService; Title = "Показать сборки"; } public bool ShowCustomAssemblies { get => _showCustomAssemblies; set { if (Set(ref _showCustomAssemblies, value)) { _messageService.InfoMessage("Показывать " + (_showCustomAssemblies ? "только пользовательские" : "все") + " сборки", nameof(ShowCustomAssemblies)); } } } private IMessageService _messageService; private bool _showCustomAssemblies; } } |
Ну и вызов:
1 2 3 4 5 6 7 8 9 10 11 | [CommandMethod("get-all-assemblies")] public static void GetAllAssembliesDialogMode() { IMessageService messageService = new MessageService(); AssembliesViewModel vm = new AssembliesViewModel(messageService); AssembliesWindow win = new AssembliesWindow() { DataContext = vm, }; Application.ShowModalWindow(win); } |
Теперь, во-первых, заголовок окна уже более вменяем. И, во-вторых, нажатие на галочку будет выводить информационное сообщение. Вдобавок с разным сообщением. Немного кривенько и косенько (в идеале nameof(ShowCustomAssemblies) прописывать, конечно, не надо, но разбираться сейчас не буду - не до того).
Ну вот, поигрались - так что можно сносить упоминания про IMessageService и вместо него подсовывать IAssemblyRepository. Код не сильно поменяется:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | using System.Reflection; using KpblcCadInfrastructure.Abstractions.Interfaces; using KpblcCadInfrastructure.Abstractions.ViewModels.Base; namespace KpblcCadInfrastructure.Core.NET.ViewModels { public class AssembliesViewModel : ViewModel { public AssembliesViewModel(IAssemblyRepository AssemblyRepository) { _assemblyRepository = AssemblyRepository; Title = "Показать сборки"; Refresh(); } public bool ShowCustomAssemblies { get => _showCustomAssemblies; set { if (Set(ref _showCustomAssemblies, value)) { Refresh(); } } } public List<Assembly> AssembliesList { get => _assembliesList; private set => Set(ref _assembliesList, value); } private void Refresh() { if (ShowCustomAssemblies) { AssembliesList = _assemblyRepository.GetCustomAssemblies().ToList(); } else { AssembliesList = _assemblyRepository.Get().ToList(); } } private IAssemblyRepository _assemblyRepository; private bool _showCustomAssemblies; private List<Assembly> _assembliesList; } } |
И в команде тоже не такие уж категорические изменения:
1 2 3 4 5 6 7 8 9 10 11 | [CommandMethod("get-all-assemblies")] public static void GetAllAssembliesDialogMode() { IAssemblyRepository assemblyRepository = new AssemblyRepository(); AssembliesViewModel vm = new AssembliesViewModel(assemblyRepository); AssembliesWindow win = new AssembliesWindow() { DataContext = vm, }; Application.ShowModalWindow(win); } |
Начинаю прописывать связку DataGrid и AssembliesList:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <Window x:Class="KpblcCadInfrastructure.Core.NET.Views.Windows.AssembliesWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:KpblcCadInfrastructure.Core.NET.Views.Windows" xmlns:vm="clr-namespace:KpblcCadInfrastructure.Core.NET.ViewModels" mc:Ignorable="d" Title="{Binding Title}" Height="250" Width="600" d:DataContext="{d:DesignInstance vm:AssembliesViewModel}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <CheckBox Grid.Row="0" Content="Показывать только внешние сборки" IsChecked="{Binding ShowCustomAssemblies}"/> <DataGrid Grid.Row="1" ItemsSource="{Binding AssembliesList}"> </DataGrid> <UniformGrid Grid.Row="2" Rows="1" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5"> <Button Content="Закрыть" IsCancel="True"></Button> </UniformGrid> </Grid> </Window> |
И вот тут выясняется, что показать те же самые Location / FullName / etc напрямую невозможно! Точнее, не так: при текущих настройках показывается овердофига столбцов. Но мне-то нужны строго определенные! Так что вношу тьму изменений. Да, профи, я знаю, что вы про меня думаете. Да, я знаю, что так делать неправильно - но мне просто хочется показать, что сразу нарисовать идеальный софт лично у меня не получается никогда.
Так что добавляю отдельный класс AssemblyInfo (пока так; возможно, потом придется переделывать):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using System; using System.Reflection; namespace KpblcCadInfrastructure.Abstractions.Entities { public class AssemblyInfo { public AssemblyInfo(Assembly Assembly) { this.Assembly = Assembly; this.Location = Assembly.Location; this.Version = Assembly.GetName(false).Version; } public string Location { get; } public Version Version { get; } public Assembly Assembly { get; } } } |
Следом меняю IAssemblyRepository на то, чтоб он возвращал уже не сборки, а экземпляры AssemblyInfo. Потратив еще минуты 2-3, правлю все остальное, и запускаю команды - просто посмотреть, насколько верно выводятся данные. Вроде бы по первым прикидкам все правильно (окно специально не правлю).
То есть уже возникают смутные подозрения, что подобное разделение логики и ответственности способно хоть где-то да помочь.
А вот теперь попробую поменять собственно команду вывода сборок в ком.строку так, чтоб она тоже использовала ViewModel (использую подход, описанный в статье "ключевые слова на нескольких языках":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | [CommandMethod("-get-all-assemblies")] public static void GetAllAssembliesCommandLineMode() { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) { return; } PromptKeywordOptions options = new PromptKeywordOptions("\nВыводить полный список [Да/Нет] <Да> : "); options.Keywords.Add("Да"); options.Keywords.Add("Yes"); options.Keywords.Add("Нет"); options.Keywords.Add("No"); options.AllowNone = true; options.AllowArbitraryInput = false; PromptResult res = doc.Editor.GetKeywords(options); if (res.Status == PromptStatus.Cancel) { return; } if (res.Status == PromptStatus.None) { res.StringResult = "Y"; } bool showAllAssemblies = res.StringResult.StartsWith("Y") || res.StringResult.StartsWith("Д"); IAssemblyInfoRepository rep = new AssemblyRepository(); AssembliesViewModel vm = new AssembliesViewModel(rep) { ShowCustomAssemblies = showAllAssemblies, }; IMessageService messageService = new MessageService(); try { foreach (AssemblyInfo assembly in vm.AssembliesList.OrderBy(o => o.Location)) { messageService.ConsoleMessage(assembly.Location); } } catch (Exception ex) { messageService.ExceptionMessage(ex); } } |
Как видно, команда nanoCAD сама определяет реализации интерфейсов и вызывает ViewModel. Та, в свою очередь, вообще никак не завязана на получение данных. И ей глубоко параллельно, куда обработанные ею данные пойдут. Лично мне такое нравится
Исходный код по состоянию на текущий момент: https://github.com/kpblc2000/KpblcCadInfrastructure
Следующая часть : https://autolisp.ru/2025/01/08/get-all-commands-part3/