Вернуть 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>

После запуска получаю нечто типа:
2024-01-23_23-51-59

В принципе, неплохо - вроде как даже что-то показывается. Уже хорошо.

С одной стороны, можно пробовать двигаться дальше, но мне кажется, что было бы неплохо все же получить реальные данные. На данный момент у меня установлены версии 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";
    }
}

Старт, и...
2024-01-24_00-05-48

"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
public MainWindowViewModel()
{
    Title = "Очистка версии";

    NCadApplicationRepository rep = new NCadApplicationRepository();
    ApplicationList = new List<NCadApplication>(rep.Get().OrderBy(o => o.Version));

    ClearCommand = new RelayCommand(OnClearCommandExecuted, CanClearCommandExecute);
}
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. Прошу сильно ногами по голове не охаживать ;)

Размещено в .NET, nanoCAD · Метки: , , , ,



Поделитесь своим мнением


Я не робот.