Вернуть nanoCAD в состояние “установили, но не запускали”
За последнюю неделю (плюс-минус) столько раз экспериментировал с nanoCAD 20+, столько раз доводил его до невменяемого состояния... И каждый раз надо было сбросить его состояние до уровня "вот только сейчас установили, дальше - начальные стандартные настройки". Ну так-то понятно: в %AppData% надо найти каталог, отвечающий за соответствующую версию, удалить; потом вызвать редактор реестра и выполнить там же аналогичные действия. Но после 7..9 цикла меня задолбало делать все вручную.
Значит, пишу программульку, ориентированную на Windows, и которую сможет запустить любой пользователь. Пока что делаю на NET Framework. Обзову, пожалуй, как KpblcNCadUserClear - ну типа очистка для текущего пользователя.
Дисклеймер: Насколько проект будет соответствовать MVVM - посмотрим. Никакого Prism / Avalonia / etc - все равно я там ни шиша не умею. Проект практически для личного пользования (по-хорошему бы еще надо определять - а не запущен ли ncad.exe, но я в эту магию не умею. Надеюсь, пока не умею). Тестов писать не хочу, нафих.
Что хорошо, так это то, что nanosoft (по крайней мере пока) данные по версиям хранит с неизменным принципом: \Software\Nanosoft\ - и внутри уже сокращенное имя платформы, и подузлами - версии. Воспользуюсь этим и создам отдельный класс для хранения всех возможных и необходимых данных по каждой платформе (да, это буду делать до того, как работать с окном):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | namespace KpblcNCadUserClear.Data { internal class NCadApplication { public NCadApplication(string RegistryHiveName) { RegistryName = RegistryHiveName; string[] splited = RegistryHiveName.Split('\'); Version = splited[splited.Length-1]; ApplicationFullName = splited[splited.Length - 2] + " " + Version; } /// <summary> Имя узла в реестре, начиная с Software </summary> public string RegistryName { get; } /// <summary> Полное имя приложения для показа в окне </summary> public string ApplicationFullName { get; } /// <summary> "Версия" приложения. Сделана строкой, т.к. может быть нечто типа "23.1 (backup)", что помешает нормальному преобразованию </summary> public string Version { get; } } } |
Теперь понадобится каким-то образом получать данные о том, что вообще существует на локальной машине у пользователя. Пока что обойдусь фейковыми данными:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using KpblcNCadUserClear.Data; using System.Collections.Generic; namespace KpblcNCadUserClear.Repositories { internal class NCadApplicationRepository { public IEnumerable<NCadApplication> Get() { return new List<NCadApplication> { new NCadApplication(@"Компьютер\HKEY_CURRENT_USER\SOFTWARE\Nanosoft\nanoCAD x64\23.1"), new NCadApplication(@"Компьютер\HKEY_CURRENT_USER\SOFTWARE\Nanosoft\nanoCAD x64\23.0"), new NCadApplication(@"Компьютер\HKEY_CURRENT_USER\SOFTWARE\Nanosoft\nanoCAD x64\24.0"), new NCadApplication(@"Компьютер\HKEY_CURRENT_USER\SOFTWARE\Nanosoft\nanoCAD x64 Plus\20") }; } } } |
Теперь по стандарту - 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 KpblcNCadUserClear.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; } } |
Забыл, что пользователь должен будет выбирать, какие версии надо "очищать", так что в класс NCadApplication надо добавить новое поле:
1 2 | /// <summary> Надо ли очищать данные </summary> public bool CheckedToClear { get; set; } |
Все, пора создавать основную VM:
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 | using KpblcNCadUserClear.Data; using KpblcNCadUserClear.Repositories; using KpblcNCadUserClear.ViewModels.Base; using System.Collections.Generic; using System.Linq; namespace KpblcNCadUserClear.ViewModels { public class MainWindowViewModel : ViewModel { public MainWindowViewModel() { Title = "Очистка версии"; NCadApplicationRepository rep = new NCadApplicationRepository(); ApplicationList = new List<NCadApplication>(rep.Get().OrderBy(o => o.Version)); } public List<NCadApplication> ApplicationList { get => _applicationList; private set => Set(ref _applicationList, value); } private List<NCadApplication> _applicationList; } } |
По ходу дела надо поменять видимость NCadApplication на public.
Теперь уже можно и с окном поиграться: добавить источник данных, прокинуть связь с заголовком, создать ListBox и посмотреть, что получается:
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 | <Window x:Class="KpblcNCadUserClear.MainWindow" 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:KpblcNCadUserClear" xmlns:vm="clr-namespace:KpblcNCadUserClear.ViewModels" mc:Ignorable="d" Title="{Binding Title}" Height="450" Width="800"> <Window.DataContext> <vm:MainWindowViewModel /> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListBox Grid.Row="0" ItemsSource="{Binding ApplicationList}"> <ListBox.ItemTemplate> <DataTemplate> <CheckBox Content="{Binding ApplicationFullName}" IsChecked="{Binding CheckedToClear}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window> |
После запуска получаю нечто типа:
В принципе, неплохо - вроде как даже что-то показывается. Уже хорошо.
С одной стороны, можно пробовать двигаться дальше, но мне кажется, что было бы неплохо все же получить реальные данные. На данный момент у меня установлены версии 23.0 (платформа и GeoniCS) и 24.0, вот и заставлю "репозиторий" выдавать реальные данные:
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 KpblcNCadUserClear.Data; using System.Collections.Generic; using System.IO; using Microsoft.Win32; namespace KpblcNCadUserClear.Repositories { internal class NCadApplicationRepository { public IEnumerable<NCadApplication> Get() { RegistryKey mainRegHive = Registry.CurrentUser.OpenSubKey(_hiveName); if (mainRegHive == null) { return null; } List<NCadApplication> res = new List<NCadApplication>(); int index = mainRegHive.Name.IndexOf(@"") + 1; foreach (string keyName in mainRegHive.GetSubKeyNames()) { using (RegistryKey subKey = mainRegHive.OpenSubKey(keyName)) { foreach (string subKeyName in subKey.GetSubKeyNames()) { res.Add(new NCadApplication(Path.Combine(subKey.Name, subKeyName).Substring(index))); } } } return res; } private readonly string _hiveName = @"Software\Nanosoft"; } } |
"Video Subsystem Performance Test" мне не нужен, достаточно того, что начинается со слова nano! Отлично, добавлю фильтрацию:
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 | using KpblcNCadUserClear.Data; using Microsoft.Win32; using System.Collections.Generic; using System.IO; namespace KpblcNCadUserClear.Repositories { internal class NCadApplicationRepository { public IEnumerable<NCadApplication> Get() { RegistryKey mainRegHive = Registry.CurrentUser.OpenSubKey(_hiveName); if (mainRegHive == null) { return null; } List<NCadApplication> res = new List<NCadApplication>(); int index = mainRegHive.Name.IndexOf(@"") + 1; foreach (string keyName in mainRegHive.GetSubKeyNames()) { if (keyName.ToUpper().StartsWith("NANO")) { using (RegistryKey subKey = mainRegHive.OpenSubKey(keyName)) { foreach (string subKeyName in subKey.GetSubKeyNames()) { res.Add(new NCadApplication(Path.Combine(subKey.Name, subKeyName).Substring(index))); } } } } return res; } private readonly string _hiveName = @"Software\Nanosoft"; } } |
Скрин уж приводить не буду, но данные попадают именно те, которые и должны быть.
Так, стоп! Я ж в самом начале сказал, что надо будет еще и с каталогами в %Appata% работать. Значит, имеет смысл добавить получение каталога в NCadApplication. Но если сделать "как хочется", можно получить очень неприятное поведение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public string AppDataSubFolder { get { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(RegistryName)) { if (key == null) { return string.Empty; } string path = key.GetValue("UserDataDir").ToString(); return path; } } } |
В таком случае при попытке хоть какого-то обращения к каталогу GeoniCS я получаю ошибку NRE, поскольку подобной записи в реестре нет. Но каталог-то в %AppData% есть. Так что попробую его назначать для такого случая принудительно, проверяя его существование:
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 | public string AppDataSubFolder { get { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(RegistryName)) { if (key == null) { return string.Empty; } var value = key.GetValue("UserDataDir"); if (value == null) { string path = Path.Combine(Environment.GetEnvironmentVariable("appdata"), ApplicationFullName); if (Directory.Exists(path)) { return path; } return string.Empty; } return key.GetValue("UserDataDir").ToString(); } } } |
Пока оставлю так, а там посмотрим. Все, пора рисовать кнопочки в окне
1 2 3 4 5 6 | <UniformGrid Grid.Row="1" Rows="1" HorizontalAlignment="Right"> <Button Content="Очистить" /> <Button Content="Отмена" /> </UniformGrid> |
И пора начинать прописывать собственно команды (если что, это один из многих варинтов, и не факт что это самый лучший):
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 | using System; using System.Windows.Input; namespace KpblcNCadUserClear.Commands.Base { internal abstract class Command : ICommand { public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public abstract bool CanExecute(object parameter); public abstract void Execute(object parameter); } } /// using KpblcNCadUserClear.Commands.Base; using System; namespace KpblcNCadUserClear.Commands { internal class RelayCommand : Command { private Action<object> _Execute; private Func<object, bool> _CanExecute; public RelayCommand(Action<object> Execute, Func<object, bool> CanExecute = null) { _Execute = Execute ?? throw new ArgumentNullException(nameof(Execute)); _CanExecute = CanExecute; } public override bool CanExecute(object parameter) { return _CanExecute?.Invoke(parameter) ?? true; } public override void Execute(object parameter) { _Execute(parameter); } } } |
Ну и делаю всего одну команду, которая очищает данные по платформе / дополнению (ну или по крайней мере пытается это сделать). Пишу напрямую в MainWindowViewModel (пока без реализации):
1 2 3 4 5 6 7 8 9 10 11 | public ICommand ClearCommand { get; } public void OnClearCommandExecuted(object p) { } public bool CanClearCommandExecute(object p) { return ApplicationList.Where(o => o.CheckedToClear).Any(); } |
И теперь можно зарегистрировать команду и прокинуть ее в окно:
1 2 3 4 5 6 7 8 9 |
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 | <Window x:Class="KpblcNCadUserClear.MainWindow" 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:KpblcNCadUserClear" xmlns:vm="clr-namespace:KpblcNCadUserClear.ViewModels" mc:Ignorable="d" Title="{Binding Title}" Height="450" Width="800"> <Window.DataContext> <vm:MainWindowViewModel /> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListBox Grid.Row="0" ItemsSource="{Binding ApplicationList}"> <ListBox.ItemTemplate> <DataTemplate> <CheckBox Content="{Binding ApplicationFullName}" IsChecked="{Binding CheckedToClear}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <UniformGrid Grid.Row="1" Rows="1" HorizontalAlignment="Right"> <Button Content="Очистить" Command="{Binding ClearCommand}" /> <Button Content="Отмена" /> </UniformGrid> </Grid> </Window> |
Какое-то время я всерьез думал - как лучше делать: либо сначала чистить реестр для каждого выбранного элемента, а потом каталог (т.е. два цикла), либо внутри одного цикла по выбранным NCadApplication сначала сносить реестр, а потом каталог. В результате, учитывая, что разница по времени выполнения минимальная, принял второй вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public void OnClearCommandExecuted(object p) { foreach (NCadApplication application in ApplicationList.Where(o => o.CheckedToClear)) { string parentPath = Path.GetDirectoryName(application.RegistryName); using (RegistryKey key = Registry.CurrentUser.OpenSubKey(parentPath, true)) { key.DeleteSubKeyTree(application.RegistryName.Substring(parentPath.Length + 1)); } if (Directory.Exists(application.AppDataSubFolder)) { Directory.Delete(application.AppDataSubFolder, true); } } } |
Теперь осталось только добавить закрытие окна, чтоб не ломать голову Ну лениво мне, честно!
1 2 3 4 5 6 7 8 | <UniformGrid Grid.Row="1" Rows="1" HorizontalAlignment="Right"> <Button Content="Очистить" Command="{Binding ClearCommand}" Click="OnCloseButtonClick"/> <Button Content="Отмена" Click="OnCloseButtonClick"/> </UniformGrid> |
И в CodeBehind
1 2 3 4 | private void OnCloseButtonClick(object sender, RoutedEventArgs e) { Close(); } |
Ну что ж, пора проверяться. Беру GeoniCS, проверяю запуск, теперь скопировать его каталог из %AppData% и экспортировать ветку реестра. Так сказать, во избежание Ну и пробую "обнулить" GeoniCS.
Повторный запуск - и появляется окно повторной установки. Вроде бы работает, ура!
Осталось только навести немного красивостей в оформлении, и все
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 52 53 54 55 56 57 58 59 60 61 62 63 | <Window x:Class="KpblcNCadUserClear.MainWindow" 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:KpblcNCadUserClear" xmlns:vm="clr-namespace:KpblcNCadUserClear.ViewModels" mc:Ignorable="d" Title="{Binding Title}" Height="450" Width="800"> <Window.DataContext> <vm:MainWindowViewModel /> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListBox Grid.Row="0" ItemsSource="{Binding ApplicationList}"> <ListBox.ItemTemplate> <DataTemplate> <CheckBox Content="{Binding ApplicationFullName}" IsChecked="{Binding CheckedToClear}" Margin="2" Padding="1" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <UniformGrid Grid.Row="1" Rows="1" HorizontalAlignment="Right"> <UniformGrid.Resources> <Style TargetType="Button"> <Setter Property="Margin" Value="4" /> <Setter Property="Padding" Value="4" /> <Style.Triggers> <Trigger Property="IsCancel" Value="True"> <Setter Property="Foreground" Value="Red" /> </Trigger> <Trigger Property="IsDefault" Value="True"> <Setter Property="FontWeight" Value="Bold" /> </Trigger> </Style.Triggers> </Style> </UniformGrid.Resources> <Button Content="Очистить" Command="{Binding ClearCommand}" Click="OnCloseButtonClick" IsDefault="True" /> <Button Content="Отмена" Click="OnCloseButtonClick" IsCancel="True" /> </UniformGrid> </Grid> </Window> |
git : https://github.com/kpblc2000/KpblcNCadUserClear.git
exe : KpblcNCadUserClear.zip
P.S. Прошу сильно ногами по голове не охаживать