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",
|
||||
"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",
|
||||
"type": "coreclr",
|
||||
|
||||
27
.vscode/tasks.json
vendored
27
.vscode/tasks.json
vendored
@@ -41,6 +41,33 @@
|
||||
},
|
||||
"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",
|
||||
"command": "dotnet",
|
||||
|
||||
23
README.md
23
README.md
@@ -1,6 +1,6 @@
|
||||
# SMS Task Two — редактор переменных среды
|
||||
|
||||
Десктопное приложение на **Avalonia** (.NET 8) для чтения и изменения пользовательских переменных среды. Ядро, ViewModels и модуль окружения вынесены в переносимые проекты для последующего порта на **WPF**.
|
||||
Десктопное приложение на **Avalonia** и **WPF** (.NET 8) для чтения и изменения пользовательских переменных среды. Ядро, ViewModels и модуль окружения вынесены в общие проекты.
|
||||
|
||||
## Solution
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
| `Sms.Environment.Linux` | `~/.config/environment.d/99-sms-task-two.conf` |
|
||||
| `Sms.TaskTwo.Core` | Конфигурация, сервис, логирование |
|
||||
| `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.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
|
||||
|
||||
- Чтение/запись: реестр `HKEY_CURRENT_USER\Environment`.
|
||||
- **Применить к сессии** (`ReloadEnvironment`): обновляет env текущего процесса и рассылает `WM_SETTINGCHANGE` для других GUI-приложений.
|
||||
|
||||
### Linux
|
||||
|
||||
- Запись в `~/.config/environment.d/` (systemd `KEY=value`).
|
||||
- **Применить к сессии** не поддерживается: переменные подхватываются после перезапуска 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-проекта.
|
||||
- Запись в `~/.config/environment.d/` (systemd `KEY=value`); переменные подхватываются после перезапуска login-сессии.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<Project Path="src/Sms.Environment.Windows/Sms.Environment.Windows.csproj" />
|
||||
<Project Path="src/Sms.Environment/Sms.Environment.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.ViewModels/Sms.TaskTwo.ViewModels.csproj" />
|
||||
</Folder>
|
||||
|
||||
@@ -52,7 +52,6 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
var managed = LoadManagedFile();
|
||||
managed[name] = value;
|
||||
WriteManagedFileAtomic(managed);
|
||||
System.Environment.SetEnvironmentVariable(name, value);
|
||||
}
|
||||
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
|
||||
{
|
||||
@@ -115,8 +114,6 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
|
||||
WriteConfFileAtomic(file, variables);
|
||||
}
|
||||
|
||||
System.Environment.SetEnvironmentVariable(name, null);
|
||||
}
|
||||
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()
|
||||
{
|
||||
if (!File.Exists(_managedFilePath))
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using Sms.Environment;
|
||||
|
||||
namespace Sms.Environment.Windows;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
{
|
||||
private const int HWND_BROADCAST = 0xffff;
|
||||
private const int WM_SETTINGCHANGE = 0x001A;
|
||||
private const string EnvironmentKeyPath = "Environment";
|
||||
|
||||
public string? Get(string name) =>
|
||||
@@ -25,8 +24,6 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
using var key = Registry.CurrentUser.OpenSubKey(EnvironmentKeyPath, writable: true)
|
||||
?? Registry.CurrentUser.CreateSubKey(EnvironmentKeyPath, writable: true);
|
||||
key.SetValue(name, value, RegistryValueKind.ExpandString);
|
||||
System.Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process);
|
||||
BroadcastEnvironmentChange();
|
||||
}
|
||||
|
||||
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);
|
||||
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)
|
||||
@@ -112,36 +88,4 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
|
||||
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);
|
||||
|
||||
void RemoveFromUserStore(string name);
|
||||
|
||||
EnvironmentReloadResult ReloadEnvironment();
|
||||
}
|
||||
|
||||
@@ -5,25 +5,6 @@
|
||||
<Application.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||
<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">
|
||||
<Setter Property="RowHeight" Value="32" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
|
||||
@@ -12,42 +12,17 @@
|
||||
Height="600"
|
||||
MinWidth="760"
|
||||
MinHeight="480"
|
||||
Background="#F5F5F5"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="NoChrome"
|
||||
SystemDecorations="None">
|
||||
Background="#F5F5F5">
|
||||
<Border CornerRadius="12"
|
||||
Background="White"
|
||||
BorderBrush="#C8C8C8"
|
||||
BorderThickness="1"
|
||||
Margin="8"
|
||||
Margin="12"
|
||||
ClipToBounds="True">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
<Border Grid.Row="0"
|
||||
Background="#ECECEC"
|
||||
CornerRadius="11,11,0,0"
|
||||
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"
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<Grid Grid.Row="0"
|
||||
Margin="12,12,12,0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<CheckBox Content="Отображать все переменные"
|
||||
IsChecked="{Binding ShowAllVariables}"
|
||||
@@ -56,13 +31,9 @@
|
||||
Content="Обновить"
|
||||
Command="{Binding RefreshCommand}"
|
||||
MinWidth="100" />
|
||||
<Button Grid.Column="2"
|
||||
Content="Применить к сессии"
|
||||
Command="{Binding ReloadEnvironmentCommand}"
|
||||
MinWidth="140" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2"
|
||||
<Grid Grid.Row="1"
|
||||
Margin="12,8,12,0"
|
||||
ColumnDefinitions="Auto,2*,3*,Auto"
|
||||
ColumnSpacing="8">
|
||||
@@ -81,19 +52,15 @@
|
||||
MinWidth="100" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="3"
|
||||
<StackPanel Grid.Row="2"
|
||||
Margin="12,4,12,0"
|
||||
Spacing="2">
|
||||
<TextBlock Foreground="#C62828"
|
||||
Text="{Binding AddVariableError}"
|
||||
IsVisible="{Binding AddVariableError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
<TextBlock Foreground="#2E7D32"
|
||||
Text="{Binding ReloadEnvironmentMessage}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding ReloadEnvironmentMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
IsVisible="{Binding HasAddVariableError}" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="4"
|
||||
<Border Grid.Row="3"
|
||||
Margin="12,4,12,12"
|
||||
CornerRadius="8"
|
||||
ClipToBounds="True"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Sms.TaskTwo.ViewModels;
|
||||
|
||||
namespace Sms.TaskTwo.Avalonia.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly Dictionary<EnvironmentVariableRowViewModel, DataGridRow> _gridRows = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -25,77 +23,25 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gridRows.TryGetValue(row, out var previousRow) && previousRow != e.Row)
|
||||
{
|
||||
previousRow.DataContextChanged -= OnRowDataContextChanged;
|
||||
}
|
||||
|
||||
_gridRows[row] = e.Row;
|
||||
ApplyRowClasses(e.Row, row);
|
||||
|
||||
row.RowAppearanceChanged -= OnRowAppearanceChanged;
|
||||
row.RowAppearanceChanged += OnRowAppearanceChanged;
|
||||
|
||||
e.Row.DataContextChanged -= OnRowDataContextChanged;
|
||||
e.Row.DataContextChanged += OnRowDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnRowDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is not DataGridRow gridRow)
|
||||
PropertyChangedEventHandler? handler = null;
|
||||
handler = (_, args) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (args.PropertyName is nameof(EnvironmentVariableRowViewModel.UseUserStore))
|
||||
{
|
||||
ApplyRowClasses(e.Row, row);
|
||||
}
|
||||
};
|
||||
|
||||
var row = _gridRows.FirstOrDefault(pair => pair.Value == gridRow).Key;
|
||||
if (row is null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
row.PropertyChanged += handler;
|
||||
e.Row.DetachedFromVisualTree += (_, _) => row.PropertyChanged -= handler;
|
||||
}
|
||||
|
||||
private static void ApplyRowClasses(DataGridRow gridRow, EnvironmentVariableRowViewModel row)
|
||||
{
|
||||
gridRow.Classes.Remove("appSettings");
|
||||
gridRow.Classes.Remove("custom");
|
||||
gridRow.Classes.Remove("userStore");
|
||||
|
||||
if (row.IsFromAppSettings)
|
||||
{
|
||||
gridRow.Classes.Add("appSettings");
|
||||
}
|
||||
|
||||
if (row.IsCustom)
|
||||
{
|
||||
gridRow.Classes.Add("custom");
|
||||
}
|
||||
|
||||
if (row.UseUserStore)
|
||||
{
|
||||
gridRow.Classes.Add("userStore");
|
||||
}
|
||||
gridRow.Classes.Set("appSettings", row.IsFromAppSettings);
|
||||
gridRow.Classes.Set("custom", row.IsCustom);
|
||||
gridRow.Classes.Set("userStore", row.UseUserStore);
|
||||
}
|
||||
|
||||
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;
|
||||
Directory.CreateDirectory(options.LogDirectory);
|
||||
var fileName = Path.Combine(
|
||||
options.LogDirectory,
|
||||
$"test-sms-wpf-app-{DateTime.Now:yyyyMMdd}.log");
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd-hh:mm:ss");
|
||||
var logFileName = SanitizeFileName($"test-sms-wpf-app-{timestamp}.log");
|
||||
var fileName = Path.Combine(options.LogDirectory, logFileName);
|
||||
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;
|
||||
_log = log;
|
||||
_configuredNames = new HashSet<string>(_options.Names, StringComparer.Ordinal);
|
||||
ReloadMetadata();
|
||||
}
|
||||
|
||||
public void EnsureConfiguredVariablesExist()
|
||||
{
|
||||
ReloadMetadata();
|
||||
|
||||
foreach (var name in _options.Names)
|
||||
{
|
||||
if (_store.IsPersistedInUserStore(name))
|
||||
@@ -51,8 +50,7 @@ public sealed class EnvironmentVariablesService
|
||||
|
||||
var value = ResolveRequiredValue(name);
|
||||
_store.Set(name, value);
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Created configured variable: {name}={value}");
|
||||
LogInfo($"Created configured variable: {name}={value}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +95,6 @@ public sealed class EnvironmentVariablesService
|
||||
|
||||
public bool TryAddCustomVariable(string name, string value, out string? errorMessage)
|
||||
{
|
||||
ReloadMetadata();
|
||||
name = name.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
@@ -127,38 +124,25 @@ public sealed class EnvironmentVariablesService
|
||||
_customNames.Add(name);
|
||||
PersistCustomNames();
|
||||
_store.Set(name, value);
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Created custom variable: {name}={value}");
|
||||
LogInfo($"Created custom variable: {name}={value}");
|
||||
|
||||
errorMessage = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public EnvironmentVariableRow GetRowSnapshot(string name)
|
||||
{
|
||||
ReloadMetadata();
|
||||
return CreateRow(name, ResolveRequiredValue(name));
|
||||
}
|
||||
public EnvironmentVariableRow GetRowSnapshot(string name) =>
|
||||
CreateRow(name, ResolveRequiredValue(name));
|
||||
|
||||
public string GetDisplayValue(string name) => ResolveRequiredValue(name);
|
||||
public string GetRequiredValue(string name) => ResolveRequiredValue(name);
|
||||
|
||||
public string? GetProcessValue(string name) =>
|
||||
_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)
|
||||
{
|
||||
var previous = _store.GetUserPersistedValue(name);
|
||||
_store.Set(name, value);
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Changed value: {name}={value} (previous: {previous ?? "<none>"})");
|
||||
LogInfo($"Changed value: {name}={value} (previous: {previous ?? "<none>"})");
|
||||
}
|
||||
|
||||
public void SaveComment(string name, string comment)
|
||||
@@ -166,22 +150,18 @@ public sealed class EnvironmentVariablesService
|
||||
var previous = _comments.GetValueOrDefault(name, string.Empty);
|
||||
_comments[name] = comment;
|
||||
PersistComments();
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Changed comment: {name}={comment} (previous: {(string.IsNullOrEmpty(previous) ? "<none>" : previous)})");
|
||||
LogInfo($"Changed comment: {name}={comment} (previous: {(string.IsNullOrEmpty(previous) ? "<none>" : previous)})");
|
||||
}
|
||||
|
||||
public void RemoveVariable(string name)
|
||||
{
|
||||
ReloadMetadata();
|
||||
|
||||
if (IsCustom(name))
|
||||
{
|
||||
_customNames.Remove(name);
|
||||
PersistCustomNames();
|
||||
_comments.Remove(name);
|
||||
PersistComments();
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Removed custom variable from list: {name}");
|
||||
LogInfo($"Removed custom variable from list: {name}");
|
||||
}
|
||||
|
||||
DeleteFromUserStore(name);
|
||||
@@ -196,8 +176,7 @@ public sealed class EnvironmentVariablesService
|
||||
|
||||
var previous = _store.GetUserPersistedValue(name);
|
||||
_store.RemoveFromUserStore(name);
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Removed from user store: {name} (previous: {previous ?? "<none>"})");
|
||||
LogInfo($"Removed from user store: {name} (previous: {previous ?? "<none>"})");
|
||||
}
|
||||
|
||||
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 (_options.Defaults.TryGetValue(name, out var defaultValue))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
return _options.Defaults.TryGetValue(name, out var defaultValue) ? defaultValue : string.Empty;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@@ -260,8 +234,7 @@ public sealed class EnvironmentVariablesService
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [WARN] Failed to parse custom variables {_options.CustomVariablesVariableName}: {ex.Message}");
|
||||
LogWarn($"Failed to parse custom variables {_options.CustomVariablesVariableName}: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -287,8 +260,7 @@ public sealed class EnvironmentVariablesService
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [WARN] Failed to parse comments variable {_options.CommentsVariableName}: {ex.Message}");
|
||||
LogWarn($"Failed to parse comments variable {_options.CommentsVariableName}: {ex.Message}");
|
||||
return new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -298,4 +270,10 @@ public sealed class EnvironmentVariablesService
|
||||
var json = JsonSerializer.Serialize(_comments, JsonOptions);
|
||||
_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;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ActualValueDisplay))]
|
||||
private string _actualValue = string.Empty;
|
||||
|
||||
public string ActualValueDisplay =>
|
||||
@@ -49,8 +50,6 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
|
||||
partial void OnUseUserStoreChanged(bool value)
|
||||
{
|
||||
RowAppearanceChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
@@ -58,9 +57,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
|
||||
if (IsFromAppSettings && !value)
|
||||
{
|
||||
BeginLoad();
|
||||
UseUserStore = true;
|
||||
EndLoad();
|
||||
RunWithoutSave(() => UseUserStore = true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,14 +76,10 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
}
|
||||
|
||||
_service.DeleteFromUserStore(Field);
|
||||
BeginLoad();
|
||||
RequiredValue = _service.GetDisplayValue(Field);
|
||||
EndLoad();
|
||||
RunWithoutSave(() => RequiredValue = _service.GetRequiredValue(Field));
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
}
|
||||
|
||||
partial void OnActualValueChanged(string value) => OnPropertyChanged(nameof(ActualValueDisplay));
|
||||
|
||||
partial void OnRequiredValueChanged(string value)
|
||||
{
|
||||
if (_isLoading || !UseUserStore)
|
||||
@@ -98,22 +91,22 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
}
|
||||
|
||||
public event EventHandler? RowAppearanceChanged;
|
||||
|
||||
public void ApplySnapshot(EnvironmentVariableRow row, bool suppressSave = false)
|
||||
{
|
||||
if (suppressSave)
|
||||
{
|
||||
BeginLoad();
|
||||
RunWithoutSave(ApplyValues);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyValues();
|
||||
}
|
||||
|
||||
RequiredValue = row.Value;
|
||||
Comment = row.Comment;
|
||||
UseUserStore = row.IsFromAppSettings || row.IsPersistedInUserStore;
|
||||
|
||||
if (suppressSave)
|
||||
void ApplyValues()
|
||||
{
|
||||
EndLoad();
|
||||
RequiredValue = row.Value;
|
||||
Comment = row.Comment;
|
||||
UseUserStore = row.IsFromAppSettings || row.IsPersistedInUserStore;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +115,6 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
ActualValue = processValue ?? string.Empty;
|
||||
}
|
||||
|
||||
public void BeginLoad() => _isLoading = true;
|
||||
|
||||
public void EndLoad() => _isLoading = false;
|
||||
|
||||
partial void OnCommentChanged(string value)
|
||||
{
|
||||
if (_isLoading)
|
||||
@@ -135,4 +124,17 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
|
||||
_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;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasAddVariableError))]
|
||||
private string? _addVariableError;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _reloadEnvironmentMessage;
|
||||
public bool HasAddVariableError => !string.IsNullOrEmpty(AddVariableError);
|
||||
|
||||
partial void OnShowAllVariablesChanged(bool value) => SyncRowsFromService();
|
||||
|
||||
partial void OnNewVariableNameChanged(string value) => AddVariableCommand.NotifyCanExecuteChanged();
|
||||
|
||||
[RelayCommand]
|
||||
private void Refresh()
|
||||
{
|
||||
SyncRowsFromService();
|
||||
RefreshProcessStates();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ReloadEnvironment()
|
||||
{
|
||||
var result = _service.ReloadEnvironment();
|
||||
ReloadEnvironmentMessage = result.Message;
|
||||
RefreshProcessStates();
|
||||
}
|
||||
private void Refresh() => SyncRowsFromService();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanAddVariable))]
|
||||
private void AddVariable()
|
||||
@@ -74,18 +62,18 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
private EnvironmentVariableRowViewModel CreateRowViewModel(EnvironmentVariableRow row)
|
||||
{
|
||||
EnvironmentVariableRowViewModel? viewModel = null;
|
||||
viewModel = new EnvironmentVariableRowViewModel(
|
||||
var field = row.Field;
|
||||
return new EnvironmentVariableRowViewModel(
|
||||
row,
|
||||
_service,
|
||||
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()
|
||||
@@ -143,11 +131,6 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
RefreshProcessStates();
|
||||
}
|
||||
|
||||
private void RefreshProcessStates()
|
||||
{
|
||||
foreach (var row in Rows)
|
||||
{
|
||||
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