Compare commits
7 Commits
095ff99cf9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 63b924e72a | |||
|
|
4cd09b7b77 | ||
|
|
f9da101c4a | ||
|
|
85e9092d14 | ||
|
|
a827bd608b | ||
| 2f56bae044 | |||
| 7e7ef33155 |
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -12,6 +12,17 @@
|
|||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"stopAtEntry": false
|
"stopAtEntry": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "WPF (Debug)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build-wpf",
|
||||||
|
"program": "${workspaceFolder}/src/Sms.TaskTwo.Wpf/bin/Debug/net8.0-windows/Sms.TaskTwo.Wpf.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Sms.TaskTwo.Wpf/bin/Debug/net8.0-windows",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Attach",
|
"name": "Attach",
|
||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
|
|||||||
27
.vscode/tasks.json
vendored
27
.vscode/tasks.json
vendored
@@ -41,6 +41,33 @@
|
|||||||
},
|
},
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "build-wpf",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "run-wpf",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj",
|
||||||
|
"--no-build"
|
||||||
|
],
|
||||||
|
"group": "none",
|
||||||
|
"problemMatcher": "$msCompile",
|
||||||
|
"dependsOn": "build-wpf"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "run-avalonia",
|
"label": "run-avalonia",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -1,6 +1,6 @@
|
|||||||
# SMS Task Two — редактор переменных среды
|
# SMS Task Two — редактор переменных среды
|
||||||
|
|
||||||
Десктопное приложение на **Avalonia** (.NET 8) для чтения и изменения пользовательских переменных среды. Ядро, ViewModels и модуль окружения вынесены в переносимые проекты для последующего порта на **WPF**.
|
Десктопное приложение на **Avalonia** и **WPF** (.NET 8) для чтения и изменения пользовательских переменных среды. Ядро, ViewModels и модуль окружения вынесены в общие проекты.
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
| `Sms.Environment.Linux` | `~/.config/environment.d/99-sms-task-two.conf` |
|
| `Sms.Environment.Linux` | `~/.config/environment.d/99-sms-task-two.conf` |
|
||||||
| `Sms.TaskTwo.Core` | Конфигурация, сервис, логирование |
|
| `Sms.TaskTwo.Core` | Конфигурация, сервис, логирование |
|
||||||
| `Sms.TaskTwo.ViewModels` | MVVM (`CommunityToolkit.Mvvm`) |
|
| `Sms.TaskTwo.ViewModels` | MVVM (`CommunityToolkit.Mvvm`) |
|
||||||
| `Sms.TaskTwo.Avalonia` | UI-хост |
|
| `Sms.TaskTwo.Avalonia` | UI-хост (Avalonia) |
|
||||||
|
| `Sms.TaskTwo.Wpf` | UI-хост (WPF) |
|
||||||
|
|
||||||
## Сборка и запуск
|
## Сборка и запуск
|
||||||
|
|
||||||
@@ -42,13 +43,13 @@ dotnet run --project src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj
|
|||||||
{"SMS_MEAL_SERVER_URL":"URL сервера","SMS_MEAL_API_KEY":"ключ API"}
|
{"SMS_MEAL_SERVER_URL":"URL сервера","SMS_MEAL_API_KEY":"ключ API"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Переменная записывается тем же механизмом, что и остальные, и доступна другим процессам после применения окружения ОС.
|
Переменная записывается тем же механизмом, что и остальные, и доступна другим процессам после перезапуска сессии или приложений.
|
||||||
|
|
||||||
## Логирование
|
## Логирование
|
||||||
|
|
||||||
Класс `Sms.TaskTwo.Core.Logging.ConsoleLog` — дублирует записи в консоль и файл.
|
Класс `Sms.TaskTwo.Core.Logging.ConsoleLog` — дублирует записи в консоль и файл.
|
||||||
|
|
||||||
По умолчанию для GUI: `logs/test-sms-wpf-app-yyyyMMdd.log`. При вызове `ConsoleLog.Open()` без имени — `test-sms-console-app-yyyyMMdd_HHmmss.log`.
|
По умолчанию для GUI: `logs/test-sms-wpf-app-yyyyMMdd-hh:mm:ss.log` (на Windows двоеточия во времени заменяются на `-`). При вызове `ConsoleLog.Open()` без имени — `test-sms-console-app-yyyyMMdd_HHmmss.log`.
|
||||||
|
|
||||||
Пример строки:
|
Пример строки:
|
||||||
|
|
||||||
@@ -72,19 +73,7 @@ dotnet run --project src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj
|
|||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
- Чтение/запись: реестр `HKEY_CURRENT_USER\Environment`.
|
- Чтение/запись: реестр `HKEY_CURRENT_USER\Environment`.
|
||||||
- **Применить к сессии** (`ReloadEnvironment`): обновляет env текущего процесса и рассылает `WM_SETTINGCHANGE` для других GUI-приложений.
|
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
- Запись в `~/.config/environment.d/` (systemd `KEY=value`).
|
- Запись в `~/.config/environment.d/` (systemd `KEY=value`); переменные подхватываются после перезапуска login-сессии.
|
||||||
- **Применить к сессии** не поддерживается: переменные подхватываются после перезапуска login-сессии.
|
|
||||||
|
|
||||||
## Предположения (ТЗ)
|
|
||||||
|
|
||||||
1. Колонка «Поле» — только имена из `appsettings.json`, без добавления новых строк вручную.
|
|
||||||
2. Значения по умолчанию показываются в UI, в ОС записываются при первом изменении пользователем.
|
|
||||||
3. Pixel-perfect вёрстка не требуется; элементы стилизованы по макету (заголовок, DataGrid, кнопки «−» / «×»).
|
|
||||||
|
|
||||||
## Порт на WPF
|
|
||||||
|
|
||||||
Создать `Sms.TaskTwo.Wpf`, подключить `Sms.TaskTwo.Core`, `Sms.TaskTwo.ViewModels`, зарегистрировать `IEnvironmentVariableStore` так же, как в `App.axaml.cs` Avalonia-проекта.
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<Project Path="src/Sms.Environment.Windows/Sms.Environment.Windows.csproj" />
|
<Project Path="src/Sms.Environment.Windows/Sms.Environment.Windows.csproj" />
|
||||||
<Project Path="src/Sms.Environment/Sms.Environment.csproj" />
|
<Project Path="src/Sms.Environment/Sms.Environment.csproj" />
|
||||||
<Project Path="src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj" />
|
<Project Path="src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj" />
|
||||||
|
<Project Path="src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj" />
|
||||||
<Project Path="src/Sms.TaskTwo.Core/Sms.TaskTwo.Core.csproj" />
|
<Project Path="src/Sms.TaskTwo.Core/Sms.TaskTwo.Core.csproj" />
|
||||||
<Project Path="src/Sms.TaskTwo.ViewModels/Sms.TaskTwo.ViewModels.csproj" />
|
<Project Path="src/Sms.TaskTwo.ViewModels/Sms.TaskTwo.ViewModels.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
|||||||
var managed = LoadManagedFile();
|
var managed = LoadManagedFile();
|
||||||
managed[name] = value;
|
managed[name] = value;
|
||||||
WriteManagedFileAtomic(managed);
|
WriteManagedFileAtomic(managed);
|
||||||
System.Environment.SetEnvironmentVariable(name, value);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
|
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
|
||||||
{
|
{
|
||||||
@@ -115,8 +114,6 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
|||||||
|
|
||||||
WriteConfFileAtomic(file, variables);
|
WriteConfFileAtomic(file, variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.Environment.SetEnvironmentVariable(name, null);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
|
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
|
||||||
{
|
{
|
||||||
@@ -126,15 +123,6 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public EnvironmentReloadResult ReloadEnvironment() =>
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message =
|
|
||||||
"Применение переменных к сессии в Linux не поддерживается. " +
|
|
||||||
"Значения сохраняются в ~/.config/environment.d/ и подхватываются после перезапуска login-сессии.",
|
|
||||||
};
|
|
||||||
|
|
||||||
private Dictionary<string, string> LoadManagedFile()
|
private Dictionary<string, string> LoadManagedFile()
|
||||||
{
|
{
|
||||||
if (!File.Exists(_managedFilePath))
|
if (!File.Exists(_managedFilePath))
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.Versioning;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using Sms.Environment;
|
using Sms.Environment;
|
||||||
|
|
||||||
namespace Sms.Environment.Windows;
|
namespace Sms.Environment.Windows;
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
||||||
{
|
{
|
||||||
private const int HWND_BROADCAST = 0xffff;
|
|
||||||
private const int WM_SETTINGCHANGE = 0x001A;
|
|
||||||
private const string EnvironmentKeyPath = "Environment";
|
private const string EnvironmentKeyPath = "Environment";
|
||||||
|
|
||||||
public string? Get(string name) =>
|
public string? Get(string name) =>
|
||||||
@@ -25,8 +24,6 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
|||||||
using var key = Registry.CurrentUser.OpenSubKey(EnvironmentKeyPath, writable: true)
|
using var key = Registry.CurrentUser.OpenSubKey(EnvironmentKeyPath, writable: true)
|
||||||
?? Registry.CurrentUser.CreateSubKey(EnvironmentKeyPath, writable: true);
|
?? Registry.CurrentUser.CreateSubKey(EnvironmentKeyPath, writable: true);
|
||||||
key.SetValue(name, value, RegistryValueKind.ExpandString);
|
key.SetValue(name, value, RegistryValueKind.ExpandString);
|
||||||
System.Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process);
|
|
||||||
BroadcastEnvironmentChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Exists(string name) => Get(name) is not null;
|
public bool Exists(string name) => Get(name) is not null;
|
||||||
@@ -79,27 +76,6 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
|||||||
{
|
{
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(EnvironmentKeyPath, writable: true);
|
using var key = Registry.CurrentUser.OpenSubKey(EnvironmentKeyPath, writable: true);
|
||||||
key?.DeleteValue(name, throwOnMissingValue: false);
|
key?.DeleteValue(name, throwOnMissingValue: false);
|
||||||
System.Environment.SetEnvironmentVariable(name, null, EnvironmentVariableTarget.Process);
|
|
||||||
BroadcastEnvironmentChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
public EnvironmentReloadResult ReloadEnvironment()
|
|
||||||
{
|
|
||||||
var variables = GetUserPersistedEnvironment();
|
|
||||||
foreach (var pair in variables)
|
|
||||||
{
|
|
||||||
System.Environment.SetEnvironmentVariable(pair.Key, pair.Value, EnvironmentVariableTarget.Process);
|
|
||||||
}
|
|
||||||
|
|
||||||
BroadcastEnvironmentChange();
|
|
||||||
|
|
||||||
return new EnvironmentReloadResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = variables.Count == 0
|
|
||||||
? "Пользовательских переменных для применения нет."
|
|
||||||
: $"Применено {variables.Count} переменных к текущему процессу и отправлено WM_SETTINGCHANGE для других приложений.",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> ToDictionary(System.Collections.IDictionary source)
|
private static Dictionary<string, string> ToDictionary(System.Collections.IDictionary source)
|
||||||
@@ -112,36 +88,4 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BroadcastEnvironmentChange()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_ = NativeMethods.SendMessageTimeout(
|
|
||||||
HWND_BROADCAST,
|
|
||||||
WM_SETTINGCHANGE,
|
|
||||||
IntPtr.Zero,
|
|
||||||
"Environment",
|
|
||||||
0,
|
|
||||||
1000,
|
|
||||||
out _);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Non-critical: new processes still see updated registry values.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NativeMethods
|
|
||||||
{
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr SendMessageTimeout(
|
|
||||||
int hWnd,
|
|
||||||
int msg,
|
|
||||||
IntPtr wParam,
|
|
||||||
string lParam,
|
|
||||||
int fuFlags,
|
|
||||||
int uTimeout,
|
|
||||||
out IntPtr lpdwResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Sms.Environment;
|
|
||||||
|
|
||||||
public sealed class EnvironmentReloadResult
|
|
||||||
{
|
|
||||||
public required bool Success { get; init; }
|
|
||||||
|
|
||||||
public required string Message { get; init; }
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,4 @@ public interface IEnvironmentVariableStore
|
|||||||
bool IsPersistedInUserStore(string name);
|
bool IsPersistedInUserStore(string name);
|
||||||
|
|
||||||
void RemoveFromUserStore(string name);
|
void RemoveFromUserStore(string name);
|
||||||
|
|
||||||
EnvironmentReloadResult ReloadEnvironment();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,6 @@
|
|||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||||
<FluentTheme />
|
<FluentTheme />
|
||||||
<Style Selector="Button.titleButton">
|
|
||||||
<Setter Property="Width" Value="40" />
|
|
||||||
<Setter Property="Height" Value="44" />
|
|
||||||
<Setter Property="Background" Value="Transparent" />
|
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
|
||||||
<Setter Property="FontSize" Value="18" />
|
|
||||||
<Setter Property="Padding" Value="0" />
|
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button.titleButton:pointerover">
|
|
||||||
<Setter Property="Background" Value="#D8D8D8" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button.titleButton.close">
|
|
||||||
<Setter Property="CornerRadius" Value="0,11,0,0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button.titleButton.close:pointerover">
|
|
||||||
<Setter Property="Background" Value="#E81123" />
|
|
||||||
<Setter Property="Foreground" Value="White" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="DataGrid.roundedGrid">
|
<Style Selector="DataGrid.roundedGrid">
|
||||||
<Setter Property="RowHeight" Value="32" />
|
<Setter Property="RowHeight" Value="32" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
|||||||
@@ -12,42 +12,17 @@
|
|||||||
Height="600"
|
Height="600"
|
||||||
MinWidth="760"
|
MinWidth="760"
|
||||||
MinHeight="480"
|
MinHeight="480"
|
||||||
Background="#F5F5F5"
|
Background="#F5F5F5">
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
|
||||||
ExtendClientAreaChromeHints="NoChrome"
|
|
||||||
SystemDecorations="None">
|
|
||||||
<Border CornerRadius="12"
|
<Border CornerRadius="12"
|
||||||
Background="White"
|
Background="White"
|
||||||
BorderBrush="#C8C8C8"
|
BorderBrush="#C8C8C8"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Margin="8"
|
Margin="12"
|
||||||
ClipToBounds="True">
|
ClipToBounds="True">
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||||
<Border Grid.Row="0"
|
<Grid Grid.Row="0"
|
||||||
Background="#ECECEC"
|
Margin="12,12,12,0"
|
||||||
CornerRadius="11,11,0,0"
|
ColumnDefinitions="*,Auto"
|
||||||
ClipToBounds="True"
|
|
||||||
Height="44">
|
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Text="{x:Static app:AppResources.WindowTitle}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="16,0,8,0"
|
|
||||||
FontSize="14" />
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Classes="titleButton"
|
|
||||||
Content="−"
|
|
||||||
Click="OnMinimizeClick" />
|
|
||||||
<Button Grid.Column="2"
|
|
||||||
Classes="titleButton close"
|
|
||||||
Content="×"
|
|
||||||
Click="OnCloseClick" />
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid Grid.Row="1"
|
|
||||||
Margin="12,8,12,0"
|
|
||||||
ColumnDefinitions="*,Auto,Auto"
|
|
||||||
ColumnSpacing="8">
|
ColumnSpacing="8">
|
||||||
<CheckBox Content="Отображать все переменные"
|
<CheckBox Content="Отображать все переменные"
|
||||||
IsChecked="{Binding ShowAllVariables}"
|
IsChecked="{Binding ShowAllVariables}"
|
||||||
@@ -56,13 +31,9 @@
|
|||||||
Content="Обновить"
|
Content="Обновить"
|
||||||
Command="{Binding RefreshCommand}"
|
Command="{Binding RefreshCommand}"
|
||||||
MinWidth="100" />
|
MinWidth="100" />
|
||||||
<Button Grid.Column="2"
|
|
||||||
Content="Применить к сессии"
|
|
||||||
Command="{Binding ReloadEnvironmentCommand}"
|
|
||||||
MinWidth="140" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="2"
|
<Grid Grid.Row="1"
|
||||||
Margin="12,8,12,0"
|
Margin="12,8,12,0"
|
||||||
ColumnDefinitions="Auto,2*,3*,Auto"
|
ColumnDefinitions="Auto,2*,3*,Auto"
|
||||||
ColumnSpacing="8">
|
ColumnSpacing="8">
|
||||||
@@ -81,19 +52,15 @@
|
|||||||
MinWidth="100" />
|
MinWidth="100" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Grid.Row="3"
|
<StackPanel Grid.Row="2"
|
||||||
Margin="12,4,12,0"
|
Margin="12,4,12,0"
|
||||||
Spacing="2">
|
Spacing="2">
|
||||||
<TextBlock Foreground="#C62828"
|
<TextBlock Foreground="#C62828"
|
||||||
Text="{Binding AddVariableError}"
|
Text="{Binding AddVariableError}"
|
||||||
IsVisible="{Binding AddVariableError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
IsVisible="{Binding HasAddVariableError}" />
|
||||||
<TextBlock Foreground="#2E7D32"
|
|
||||||
Text="{Binding ReloadEnvironmentMessage}"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
IsVisible="{Binding ReloadEnvironmentMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border Grid.Row="4"
|
<Border Grid.Row="3"
|
||||||
Margin="12,4,12,12"
|
Margin="12,4,12,12"
|
||||||
CornerRadius="8"
|
CornerRadius="8"
|
||||||
ClipToBounds="True"
|
ClipToBounds="True"
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Sms.TaskTwo.ViewModels;
|
using Sms.TaskTwo.ViewModels;
|
||||||
|
|
||||||
namespace Sms.TaskTwo.Avalonia.Views;
|
namespace Sms.TaskTwo.Avalonia.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly Dictionary<EnvironmentVariableRowViewModel, DataGridRow> _gridRows = new();
|
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -25,77 +23,25 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_gridRows.TryGetValue(row, out var previousRow) && previousRow != e.Row)
|
|
||||||
{
|
|
||||||
previousRow.DataContextChanged -= OnRowDataContextChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gridRows[row] = e.Row;
|
|
||||||
ApplyRowClasses(e.Row, row);
|
ApplyRowClasses(e.Row, row);
|
||||||
|
|
||||||
row.RowAppearanceChanged -= OnRowAppearanceChanged;
|
PropertyChangedEventHandler? handler = null;
|
||||||
row.RowAppearanceChanged += OnRowAppearanceChanged;
|
handler = (_, args) =>
|
||||||
|
|
||||||
e.Row.DataContextChanged -= OnRowDataContextChanged;
|
|
||||||
e.Row.DataContextChanged += OnRowDataContextChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRowDataContextChanged(object? sender, EventArgs e)
|
|
||||||
{
|
{
|
||||||
if (sender is not DataGridRow gridRow)
|
if (args.PropertyName is nameof(EnvironmentVariableRowViewModel.UseUserStore))
|
||||||
{
|
{
|
||||||
return;
|
ApplyRowClasses(e.Row, row);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var row = _gridRows.FirstOrDefault(pair => pair.Value == gridRow).Key;
|
row.PropertyChanged += handler;
|
||||||
if (row is null)
|
e.Row.DetachedFromVisualTree += (_, _) => row.PropertyChanged -= handler;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.RowAppearanceChanged -= OnRowAppearanceChanged;
|
|
||||||
_gridRows.Remove(row);
|
|
||||||
gridRow.DataContextChanged -= OnRowDataContextChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRowAppearanceChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not EnvironmentVariableRowViewModel row)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gridRows.TryGetValue(row, out var gridRow))
|
|
||||||
{
|
|
||||||
ApplyRowClasses(gridRow, row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyRowClasses(DataGridRow gridRow, EnvironmentVariableRowViewModel row)
|
private static void ApplyRowClasses(DataGridRow gridRow, EnvironmentVariableRowViewModel row)
|
||||||
{
|
{
|
||||||
gridRow.Classes.Remove("appSettings");
|
gridRow.Classes.Set("appSettings", row.IsFromAppSettings);
|
||||||
gridRow.Classes.Remove("custom");
|
gridRow.Classes.Set("custom", row.IsCustom);
|
||||||
gridRow.Classes.Remove("userStore");
|
gridRow.Classes.Set("userStore", row.UseUserStore);
|
||||||
|
|
||||||
if (row.IsFromAppSettings)
|
|
||||||
{
|
|
||||||
gridRow.Classes.Add("appSettings");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.IsCustom)
|
|
||||||
{
|
|
||||||
gridRow.Classes.Add("custom");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.UseUserStore)
|
|
||||||
{
|
|
||||||
gridRow.Classes.Add("userStore");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMinimizeClick(object? sender, RoutedEventArgs e) =>
|
|
||||||
WindowState = WindowState.Minimized;
|
|
||||||
|
|
||||||
private void OnCloseClick(object? sender, RoutedEventArgs e) =>
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
var options = serviceProvider.GetRequiredService<IOptions<LoggingOptions>>().Value;
|
var options = serviceProvider.GetRequiredService<IOptions<LoggingOptions>>().Value;
|
||||||
Directory.CreateDirectory(options.LogDirectory);
|
Directory.CreateDirectory(options.LogDirectory);
|
||||||
var fileName = Path.Combine(
|
var timestamp = DateTime.Now.ToString("yyyyMMdd-hh:mm:ss");
|
||||||
options.LogDirectory,
|
var logFileName = SanitizeFileName($"test-sms-wpf-app-{timestamp}.log");
|
||||||
$"test-sms-wpf-app-{DateTime.Now:yyyyMMdd}.log");
|
var fileName = Path.Combine(options.LogDirectory, logFileName);
|
||||||
return ConsoleLog.Open(fileName);
|
return ConsoleLog.Open(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string fileName) =>
|
||||||
|
string.Concat(fileName.Select(static c =>
|
||||||
|
Path.GetInvalidFileNameChars().Contains(c) ? '-' : c));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,11 @@ public sealed class EnvironmentVariablesService
|
|||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_log = log;
|
_log = log;
|
||||||
_configuredNames = new HashSet<string>(_options.Names, StringComparer.Ordinal);
|
_configuredNames = new HashSet<string>(_options.Names, StringComparer.Ordinal);
|
||||||
|
ReloadMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void EnsureConfiguredVariablesExist()
|
public void EnsureConfiguredVariablesExist()
|
||||||
{
|
{
|
||||||
ReloadMetadata();
|
|
||||||
|
|
||||||
foreach (var name in _options.Names)
|
foreach (var name in _options.Names)
|
||||||
{
|
{
|
||||||
if (_store.IsPersistedInUserStore(name))
|
if (_store.IsPersistedInUserStore(name))
|
||||||
@@ -51,8 +50,7 @@ public sealed class EnvironmentVariablesService
|
|||||||
|
|
||||||
var value = ResolveRequiredValue(name);
|
var value = ResolveRequiredValue(name);
|
||||||
_store.Set(name, value);
|
_store.Set(name, value);
|
||||||
_log.WriteLine(
|
LogInfo($"Created configured variable: {name}={value}");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Created configured variable: {name}={value}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +95,6 @@ public sealed class EnvironmentVariablesService
|
|||||||
|
|
||||||
public bool TryAddCustomVariable(string name, string value, out string? errorMessage)
|
public bool TryAddCustomVariable(string name, string value, out string? errorMessage)
|
||||||
{
|
{
|
||||||
ReloadMetadata();
|
|
||||||
name = name.Trim();
|
name = name.Trim();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
@@ -127,38 +124,25 @@ public sealed class EnvironmentVariablesService
|
|||||||
_customNames.Add(name);
|
_customNames.Add(name);
|
||||||
PersistCustomNames();
|
PersistCustomNames();
|
||||||
_store.Set(name, value);
|
_store.Set(name, value);
|
||||||
_log.WriteLine(
|
LogInfo($"Created custom variable: {name}={value}");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Created custom variable: {name}={value}");
|
|
||||||
|
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EnvironmentVariableRow GetRowSnapshot(string name)
|
public EnvironmentVariableRow GetRowSnapshot(string name) =>
|
||||||
{
|
CreateRow(name, ResolveRequiredValue(name));
|
||||||
ReloadMetadata();
|
|
||||||
return CreateRow(name, ResolveRequiredValue(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetDisplayValue(string name) => ResolveRequiredValue(name);
|
public string GetRequiredValue(string name) => ResolveRequiredValue(name);
|
||||||
|
|
||||||
public string? GetProcessValue(string name) =>
|
public string? GetProcessValue(string name) =>
|
||||||
_store.GetProcessEnvironment().TryGetValue(name, out var value) ? value : null;
|
_store.GetProcessEnvironment().TryGetValue(name, out var value) ? value : null;
|
||||||
|
|
||||||
public EnvironmentReloadResult ReloadEnvironment()
|
|
||||||
{
|
|
||||||
var result = _store.ReloadEnvironment();
|
|
||||||
_log.WriteLine(
|
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Reload environment: success={result.Success}; {result.Message}");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveValue(string name, string value)
|
public void SaveValue(string name, string value)
|
||||||
{
|
{
|
||||||
var previous = _store.GetUserPersistedValue(name);
|
var previous = _store.GetUserPersistedValue(name);
|
||||||
_store.Set(name, value);
|
_store.Set(name, value);
|
||||||
_log.WriteLine(
|
LogInfo($"Changed value: {name}={value} (previous: {previous ?? "<none>"})");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Changed value: {name}={value} (previous: {previous ?? "<none>"})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveComment(string name, string comment)
|
public void SaveComment(string name, string comment)
|
||||||
@@ -166,22 +150,18 @@ public sealed class EnvironmentVariablesService
|
|||||||
var previous = _comments.GetValueOrDefault(name, string.Empty);
|
var previous = _comments.GetValueOrDefault(name, string.Empty);
|
||||||
_comments[name] = comment;
|
_comments[name] = comment;
|
||||||
PersistComments();
|
PersistComments();
|
||||||
_log.WriteLine(
|
LogInfo($"Changed comment: {name}={comment} (previous: {(string.IsNullOrEmpty(previous) ? "<none>" : previous)})");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Changed comment: {name}={comment} (previous: {(string.IsNullOrEmpty(previous) ? "<none>" : previous)})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveVariable(string name)
|
public void RemoveVariable(string name)
|
||||||
{
|
{
|
||||||
ReloadMetadata();
|
|
||||||
|
|
||||||
if (IsCustom(name))
|
if (IsCustom(name))
|
||||||
{
|
{
|
||||||
_customNames.Remove(name);
|
_customNames.Remove(name);
|
||||||
PersistCustomNames();
|
PersistCustomNames();
|
||||||
_comments.Remove(name);
|
_comments.Remove(name);
|
||||||
PersistComments();
|
PersistComments();
|
||||||
_log.WriteLine(
|
LogInfo($"Removed custom variable from list: {name}");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Removed custom variable from list: {name}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteFromUserStore(name);
|
DeleteFromUserStore(name);
|
||||||
@@ -196,8 +176,7 @@ public sealed class EnvironmentVariablesService
|
|||||||
|
|
||||||
var previous = _store.GetUserPersistedValue(name);
|
var previous = _store.GetUserPersistedValue(name);
|
||||||
_store.RemoveFromUserStore(name);
|
_store.RemoveFromUserStore(name);
|
||||||
_log.WriteLine(
|
LogInfo($"Removed from user store: {name} (previous: {previous ?? "<none>"})");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Removed from user store: {name} (previous: {previous ?? "<none>"})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsCustom(string name) => _customNames.Contains(name, StringComparer.Ordinal);
|
public bool IsCustom(string name) => _customNames.Contains(name, StringComparer.Ordinal);
|
||||||
@@ -235,12 +214,7 @@ public sealed class EnvironmentVariablesService
|
|||||||
|
|
||||||
if (_configuredNames.Contains(name) || _customNames.Contains(name))
|
if (_configuredNames.Contains(name) || _customNames.Contains(name))
|
||||||
{
|
{
|
||||||
if (_options.Defaults.TryGetValue(name, out var defaultValue))
|
return _options.Defaults.TryGetValue(name, out var defaultValue) ? defaultValue : string.Empty;
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
@@ -260,8 +234,7 @@ public sealed class EnvironmentVariablesService
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_log.WriteLine(
|
LogWarn($"Failed to parse custom variables {_options.CustomVariablesVariableName}: {ex.Message}");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [WARN] Failed to parse custom variables {_options.CustomVariablesVariableName}: {ex.Message}");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,8 +260,7 @@ public sealed class EnvironmentVariablesService
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_log.WriteLine(
|
LogWarn($"Failed to parse comments variable {_options.CommentsVariableName}: {ex.Message}");
|
||||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [WARN] Failed to parse comments variable {_options.CommentsVariableName}: {ex.Message}");
|
|
||||||
return new Dictionary<string, string>(StringComparer.Ordinal);
|
return new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,4 +270,10 @@ public sealed class EnvironmentVariablesService
|
|||||||
var json = JsonSerializer.Serialize(_comments, JsonOptions);
|
var json = JsonSerializer.Serialize(_comments, JsonOptions);
|
||||||
_store.Set(_options.CommentsVariableName, json);
|
_store.Set(_options.CommentsVariableName, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogInfo(string message) =>
|
||||||
|
_log.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] {message}");
|
||||||
|
|
||||||
|
private void LogWarn(string message) =>
|
||||||
|
_log.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [WARN] {message}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
private bool _useUserStore;
|
private bool _useUserStore;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ActualValueDisplay))]
|
||||||
private string _actualValue = string.Empty;
|
private string _actualValue = string.Empty;
|
||||||
|
|
||||||
public string ActualValueDisplay =>
|
public string ActualValueDisplay =>
|
||||||
@@ -49,8 +50,6 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
|
|
||||||
partial void OnUseUserStoreChanged(bool value)
|
partial void OnUseUserStoreChanged(bool value)
|
||||||
{
|
{
|
||||||
RowAppearanceChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
|
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -58,9 +57,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
|
|
||||||
if (IsFromAppSettings && !value)
|
if (IsFromAppSettings && !value)
|
||||||
{
|
{
|
||||||
BeginLoad();
|
RunWithoutSave(() => UseUserStore = true);
|
||||||
UseUserStore = true;
|
|
||||||
EndLoad();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +76,10 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
_service.DeleteFromUserStore(Field);
|
_service.DeleteFromUserStore(Field);
|
||||||
BeginLoad();
|
RunWithoutSave(() => RequiredValue = _service.GetRequiredValue(Field));
|
||||||
RequiredValue = _service.GetDisplayValue(Field);
|
|
||||||
EndLoad();
|
|
||||||
RefreshActualValue(_service.GetProcessValue(Field));
|
RefreshActualValue(_service.GetProcessValue(Field));
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnActualValueChanged(string value) => OnPropertyChanged(nameof(ActualValueDisplay));
|
|
||||||
|
|
||||||
partial void OnRequiredValueChanged(string value)
|
partial void OnRequiredValueChanged(string value)
|
||||||
{
|
{
|
||||||
if (_isLoading || !UseUserStore)
|
if (_isLoading || !UseUserStore)
|
||||||
@@ -98,22 +91,22 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
RefreshActualValue(_service.GetProcessValue(Field));
|
RefreshActualValue(_service.GetProcessValue(Field));
|
||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler? RowAppearanceChanged;
|
|
||||||
|
|
||||||
public void ApplySnapshot(EnvironmentVariableRow row, bool suppressSave = false)
|
public void ApplySnapshot(EnvironmentVariableRow row, bool suppressSave = false)
|
||||||
{
|
{
|
||||||
if (suppressSave)
|
if (suppressSave)
|
||||||
{
|
{
|
||||||
BeginLoad();
|
RunWithoutSave(ApplyValues);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ApplyValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ApplyValues()
|
||||||
|
{
|
||||||
RequiredValue = row.Value;
|
RequiredValue = row.Value;
|
||||||
Comment = row.Comment;
|
Comment = row.Comment;
|
||||||
UseUserStore = row.IsFromAppSettings || row.IsPersistedInUserStore;
|
UseUserStore = row.IsFromAppSettings || row.IsPersistedInUserStore;
|
||||||
|
|
||||||
if (suppressSave)
|
|
||||||
{
|
|
||||||
EndLoad();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +115,6 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
ActualValue = processValue ?? string.Empty;
|
ActualValue = processValue ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginLoad() => _isLoading = true;
|
|
||||||
|
|
||||||
public void EndLoad() => _isLoading = false;
|
|
||||||
|
|
||||||
partial void OnCommentChanged(string value)
|
partial void OnCommentChanged(string value)
|
||||||
{
|
{
|
||||||
if (_isLoading)
|
if (_isLoading)
|
||||||
@@ -135,4 +124,17 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
|||||||
|
|
||||||
_service.SaveComment(Field, value);
|
_service.SaveComment(Field, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RunWithoutSave(Action action)
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,29 +30,17 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
|||||||
private string _newVariableValue = string.Empty;
|
private string _newVariableValue = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasAddVariableError))]
|
||||||
private string? _addVariableError;
|
private string? _addVariableError;
|
||||||
|
|
||||||
[ObservableProperty]
|
public bool HasAddVariableError => !string.IsNullOrEmpty(AddVariableError);
|
||||||
private string? _reloadEnvironmentMessage;
|
|
||||||
|
|
||||||
partial void OnShowAllVariablesChanged(bool value) => SyncRowsFromService();
|
partial void OnShowAllVariablesChanged(bool value) => SyncRowsFromService();
|
||||||
|
|
||||||
partial void OnNewVariableNameChanged(string value) => AddVariableCommand.NotifyCanExecuteChanged();
|
partial void OnNewVariableNameChanged(string value) => AddVariableCommand.NotifyCanExecuteChanged();
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Refresh()
|
private void Refresh() => SyncRowsFromService();
|
||||||
{
|
|
||||||
SyncRowsFromService();
|
|
||||||
RefreshProcessStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void ReloadEnvironment()
|
|
||||||
{
|
|
||||||
var result = _service.ReloadEnvironment();
|
|
||||||
ReloadEnvironmentMessage = result.Message;
|
|
||||||
RefreshProcessStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanAddVariable))]
|
[RelayCommand(CanExecute = nameof(CanAddVariable))]
|
||||||
private void AddVariable()
|
private void AddVariable()
|
||||||
@@ -74,18 +62,18 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
|||||||
|
|
||||||
private EnvironmentVariableRowViewModel CreateRowViewModel(EnvironmentVariableRow row)
|
private EnvironmentVariableRowViewModel CreateRowViewModel(EnvironmentVariableRow row)
|
||||||
{
|
{
|
||||||
EnvironmentVariableRowViewModel? viewModel = null;
|
var field = row.Field;
|
||||||
viewModel = new EnvironmentVariableRowViewModel(
|
return new EnvironmentVariableRowViewModel(
|
||||||
row,
|
row,
|
||||||
_service,
|
_service,
|
||||||
onCustomRemoved: () =>
|
onCustomRemoved: () =>
|
||||||
{
|
{
|
||||||
if (viewModel is not null)
|
var existing = Rows.FirstOrDefault(r => r.Field == field);
|
||||||
|
if (existing is not null)
|
||||||
{
|
{
|
||||||
Rows.Remove(viewModel);
|
Rows.Remove(existing);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return viewModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int FindInsertIndexForCustomVariable()
|
private int FindInsertIndexForCustomVariable()
|
||||||
@@ -143,11 +131,6 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshProcessStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshProcessStates()
|
|
||||||
{
|
|
||||||
foreach (var row in Rows)
|
foreach (var row in Rows)
|
||||||
{
|
{
|
||||||
row.RefreshActualValue(_service.GetProcessValue(row.Field));
|
row.RefreshActualValue(_service.GetProcessValue(row.Field));
|
||||||
|
|||||||
70
src/Sms.TaskTwo.Wpf/App.xaml
Normal file
70
src/Sms.TaskTwo.Wpf/App.xaml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<Application x:Class="Sms.TaskTwo.Wpf.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Startup="OnStartup"
|
||||||
|
Exit="OnExit">
|
||||||
|
<Application.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
|
||||||
|
<Style x:Key="RoundedGridStyle" TargetType="DataGrid">
|
||||||
|
<Setter Property="RowHeight" Value="32" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="GridLinesVisibility" Value="All" />
|
||||||
|
<Setter Property="HeadersVisibility" Value="Column" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="CanUserReorderColumns" Value="False" />
|
||||||
|
<Setter Property="CanUserResizeColumns" Value="True" />
|
||||||
|
<Setter Property="CanUserSortColumns" Value="False" />
|
||||||
|
<Setter Property="AutoGenerateColumns" Value="False" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="VariableRowStyle" TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsFromAppSettings}" Value="True">
|
||||||
|
<Setter Property="Background" Value="#E3F2FD" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding IsCustom}" Value="True">
|
||||||
|
<Setter Property="Background" Value="#FFF8E1" />
|
||||||
|
</DataTrigger>
|
||||||
|
<MultiDataTrigger>
|
||||||
|
<MultiDataTrigger.Conditions>
|
||||||
|
<Condition Binding="{Binding IsFromAppSettings}" Value="True" />
|
||||||
|
<Condition Binding="{Binding UseUserStore}" Value="True" />
|
||||||
|
</MultiDataTrigger.Conditions>
|
||||||
|
<Setter Property="Background" Value="#C8E6C9" />
|
||||||
|
</MultiDataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding UseUserStore}" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="#2E7D32" />
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,0,2" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="WatermarkTextBlockStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="IsHitTestVisible" Value="False" />
|
||||||
|
<Setter Property="Margin" Value="6,0,0,0" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="Foreground" Value="#9E9E9E" />
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="NameWatermarkStyle" TargetType="TextBlock" BasedOn="{StaticResource WatermarkTextBlockStyle}">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding NewVariableName}" Value="">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="ValueWatermarkStyle" TargetType="TextBlock" BasedOn="{StaticResource WatermarkTextBlockStyle}">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding NewVariableValue}" Value="">
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter Property="Padding" Value="8,4" />
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
54
src/Sms.TaskTwo.Wpf/App.xaml.cs
Normal file
54
src/Sms.TaskTwo.Wpf/App.xaml.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Sms.Environment;
|
||||||
|
using Sms.Environment.Linux;
|
||||||
|
using Sms.Environment.Windows;
|
||||||
|
using Sms.TaskTwo.Core.DependencyInjection;
|
||||||
|
using Sms.TaskTwo.Core.Logging;
|
||||||
|
using Sms.TaskTwo.ViewModels;
|
||||||
|
using Sms.TaskTwo.Wpf.Views;
|
||||||
|
|
||||||
|
namespace Sms.TaskTwo.Wpf;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public static IServiceProvider Services { get; private set; } = null!;
|
||||||
|
|
||||||
|
private void OnStartup(object sender, StartupEventArgs e)
|
||||||
|
{
|
||||||
|
Services = ConfigureServices();
|
||||||
|
var mainViewModel = Services.GetRequiredService<MainWindowViewModel>();
|
||||||
|
MainWindow = new MainWindow(mainViewModel);
|
||||||
|
MainWindow.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExit(object sender, ExitEventArgs e)
|
||||||
|
{
|
||||||
|
if (Services.GetService<ConsoleLog>() is IDisposable log)
|
||||||
|
{
|
||||||
|
log.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceProvider ConfigureServices()
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
|
services.AddSingleton<IEnvironmentVariableStore>(CreateEnvironmentStore);
|
||||||
|
services.AddTaskTwoCore(configuration);
|
||||||
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
|
|
||||||
|
return services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnvironmentVariableStore CreateEnvironmentStore(IServiceProvider _) =>
|
||||||
|
OperatingSystem.IsWindows()
|
||||||
|
? new WindowsEnvironmentVariableStore()
|
||||||
|
: new LinuxEnvironmentVariableStore();
|
||||||
|
}
|
||||||
28
src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj
Normal file
28
src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Sms.Environment.Linux\Sms.Environment.Linux.csproj" />
|
||||||
|
<ProjectReference Include="..\Sms.Environment.Windows\Sms.Environment.Windows.csproj" />
|
||||||
|
<ProjectReference Include="..\Sms.TaskTwo.Core\Sms.TaskTwo.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Sms.TaskTwo.ViewModels\Sms.TaskTwo.ViewModels.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
119
src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml
Normal file
119
src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<Window x:Class="Sms.TaskTwo.Wpf.Views.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:Sms.TaskTwo.ViewModels;assembly=Sms.TaskTwo.ViewModels"
|
||||||
|
Title="Тестовое WPF-приложение для SmartMealService"
|
||||||
|
Width="960"
|
||||||
|
Height="600"
|
||||||
|
MinWidth="760"
|
||||||
|
MinHeight="480"
|
||||||
|
Background="#F5F5F5">
|
||||||
|
<Border CornerRadius="12"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#C8C8C8"
|
||||||
|
BorderThickness="1"
|
||||||
|
Margin="12"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Row="0"
|
||||||
|
Margin="12,12,12,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<CheckBox Content="Отображать все переменные"
|
||||||
|
IsChecked="{Binding ShowAllVariables}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Content="Обновить"
|
||||||
|
Command="{Binding RefreshCommand}"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="8,0,0,0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1"
|
||||||
|
Margin="12,8,12,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="3*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="Новая:"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<Grid Grid.Column="1" Margin="8,0,0,0">
|
||||||
|
<TextBox Text="{Binding NewVariableName, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBlock Text="Имя переменной" Style="{StaticResource NameWatermarkStyle}" />
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Column="2" Margin="8,0,0,0">
|
||||||
|
<TextBox Text="{Binding NewVariableValue, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBlock Text="Значение" Style="{StaticResource ValueWatermarkStyle}" />
|
||||||
|
</Grid>
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Content="Добавить"
|
||||||
|
Command="{Binding AddVariableCommand}"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="8,0,0,0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Margin="12,4,12,0"
|
||||||
|
Foreground="#C62828"
|
||||||
|
Text="{Binding AddVariableError}"
|
||||||
|
Visibility="{Binding HasAddVariableError, Converter={StaticResource BooleanToVisibility}}" />
|
||||||
|
|
||||||
|
<Border Grid.Row="3"
|
||||||
|
Margin="12,4,12,12"
|
||||||
|
CornerRadius="8"
|
||||||
|
ClipToBounds="True"
|
||||||
|
Background="White"
|
||||||
|
BorderBrush="#B0B0B0"
|
||||||
|
BorderThickness="1">
|
||||||
|
<DataGrid x:Name="VariablesGrid"
|
||||||
|
Style="{StaticResource RoundedGridStyle}"
|
||||||
|
RowStyle="{StaticResource VariableRowStyle}"
|
||||||
|
ItemsSource="{Binding Rows}">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Header=""
|
||||||
|
Width="44">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type vm:EnvironmentVariableRowViewModel}">
|
||||||
|
<CheckBox IsChecked="{Binding UseUserStore, Mode=TwoWay}"
|
||||||
|
IsEnabled="{Binding CanChangeUserStore}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTextColumn Header="Поле"
|
||||||
|
Binding="{Binding Field}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Width="210"
|
||||||
|
MinWidth="150" />
|
||||||
|
<DataGridTextColumn Header="Требуемое значение"
|
||||||
|
Binding="{Binding RequiredValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Width="2*"
|
||||||
|
MinWidth="140" />
|
||||||
|
<DataGridTextColumn Header="Актуальное значение"
|
||||||
|
Binding="{Binding ActualValueDisplay}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Width="2*"
|
||||||
|
MinWidth="140" />
|
||||||
|
<DataGridTextColumn Header="Комментарий"
|
||||||
|
Binding="{Binding Comment, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Width="240"
|
||||||
|
MinWidth="180" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
13
src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml.cs
Normal file
13
src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using Sms.TaskTwo.ViewModels;
|
||||||
|
|
||||||
|
namespace Sms.TaskTwo.Wpf.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow(MainWindowViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Sms.TaskTwo.Wpf/app.manifest
Normal file
10
src/Sms.TaskTwo.Wpf/app.manifest
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="Sms.TaskTwo.Wpf.Desktop"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
16
src/Sms.TaskTwo.Wpf/appsettings.json
Normal file
16
src/Sms.TaskTwo.Wpf/appsettings.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"EnvironmentVariables": {
|
||||||
|
"Names": [
|
||||||
|
"SMS_MEAL_SERVER_URL",
|
||||||
|
"SMS_MEAL_API_KEY"
|
||||||
|
],
|
||||||
|
"CommentsVariableName": "SMS_TASK_TWO_COMMENTS",
|
||||||
|
"CustomVariablesVariableName": "SMS_TASK_TWO_CUSTOM_VARS",
|
||||||
|
"Defaults": {
|
||||||
|
"SMS_MEAL_SERVER_URL": "https://localhost"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogDirectory": "logs"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user