diff --git a/.vscode/launch.json b/.vscode/launch.json index cce1b42..0d92f7f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ad2a49b..083bd9b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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", diff --git a/README.md b/README.md index ecb7654..9bf921d 100644 --- a/README.md +++ b/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) | ## Сборка и запуск @@ -83,6 +84,10 @@ dotnet run --project src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj 2. Значения по умолчанию показываются в UI, в ОС записываются при первом изменении пользователем. 3. Pixel-perfect вёрстка не требуется; элементы стилизованы по макету (заголовок, DataGrid, кнопки «−» / «×»). -## Порт на WPF +## WPF -Создать `Sms.TaskTwo.Wpf`, подключить `Sms.TaskTwo.Core`, `Sms.TaskTwo.ViewModels`, зарегистрировать `IEnvironmentVariableStore` так же, как в `App.axaml.cs` Avalonia-проекта. +```bash +dotnet run --project src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj +``` + +Конфигурация: [`src/Sms.TaskTwo.Wpf/appsettings.json`](src/Sms.TaskTwo.Wpf/appsettings.json) (аналогично Avalonia). diff --git a/Sms.TaskTwo.slnx b/Sms.TaskTwo.slnx index d2ddeab..9b4f5d6 100644 --- a/Sms.TaskTwo.slnx +++ b/Sms.TaskTwo.slnx @@ -4,6 +4,7 @@ + diff --git a/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml b/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml index 5e848a1..15cef0b 100644 --- a/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml +++ b/src/Sms.TaskTwo.Avalonia/Views/MainWindow.axaml @@ -99,6 +99,11 @@ Binding="{Binding RequiredValue, Mode=TwoWay}" Width="2*" MinWidth="140" /> + ResolveRequiredValue(name); + public string? GetProcessValue(string name) => + _store.GetProcessEnvironment().TryGetValue(name, out var value) ? value : null; + public void SaveValue(string name, string value) { var previous = _store.GetUserPersistedValue(name); diff --git a/src/Sms.TaskTwo.ViewModels/EnvironmentVariableRowViewModel.cs b/src/Sms.TaskTwo.ViewModels/EnvironmentVariableRowViewModel.cs index ab03e15..ed3f66b 100644 --- a/src/Sms.TaskTwo.ViewModels/EnvironmentVariableRowViewModel.cs +++ b/src/Sms.TaskTwo.ViewModels/EnvironmentVariableRowViewModel.cs @@ -21,6 +21,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject _service = service; _onCustomRemoved = onCustomRemoved; ApplySnapshot(row, suppressSave: true); + RefreshActualValue(_service.GetProcessValue(Field)); } public string Field { get; } @@ -34,6 +35,12 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject [ObservableProperty] private bool _useUserStore; + [ObservableProperty] + private string _actualValue = string.Empty; + + public string ActualValueDisplay => + string.IsNullOrEmpty(ActualValue) ? "<нет>" : ActualValue; + [ObservableProperty] private string _requiredValue = string.Empty; @@ -60,6 +67,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject if (value) { _service.SaveValue(Field, RequiredValue); + RefreshActualValue(_service.GetProcessValue(Field)); return; } @@ -74,8 +82,11 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject BeginLoad(); RequiredValue = _service.GetDisplayValue(Field); EndLoad(); + RefreshActualValue(_service.GetProcessValue(Field)); } + partial void OnActualValueChanged(string value) => OnPropertyChanged(nameof(ActualValueDisplay)); + partial void OnRequiredValueChanged(string value) { if (_isLoading || !UseUserStore) @@ -84,6 +95,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject } _service.SaveValue(Field, value); + RefreshActualValue(_service.GetProcessValue(Field)); } public event EventHandler? RowAppearanceChanged; @@ -105,6 +117,11 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject } } + public void RefreshActualValue(string? processValue) + { + ActualValue = processValue ?? string.Empty; + } + public void BeginLoad() => _isLoading = true; public void EndLoad() => _isLoading = false; diff --git a/src/Sms.TaskTwo.ViewModels/MainWindowViewModel.cs b/src/Sms.TaskTwo.ViewModels/MainWindowViewModel.cs index 0375353..a1ce390 100644 --- a/src/Sms.TaskTwo.ViewModels/MainWindowViewModel.cs +++ b/src/Sms.TaskTwo.ViewModels/MainWindowViewModel.cs @@ -127,6 +127,16 @@ public sealed partial class MainWindowViewModel : ObservableObject MoveItem(Rows, currentIndex, targetIndex); } } + + RefreshProcessStates(); + } + + private void RefreshProcessStates() + { + foreach (var row in Rows) + { + row.RefreshActualValue(_service.GetProcessValue(row.Field)); + } } private static void MoveItem( diff --git a/src/Sms.TaskTwo.Wpf/App.xaml b/src/Sms.TaskTwo.Wpf/App.xaml new file mode 100644 index 0000000..d523f36 --- /dev/null +++ b/src/Sms.TaskTwo.Wpf/App.xaml @@ -0,0 +1,24 @@ + + + + + + diff --git a/src/Sms.TaskTwo.Wpf/App.xaml.cs b/src/Sms.TaskTwo.Wpf/App.xaml.cs new file mode 100644 index 0000000..88878b1 --- /dev/null +++ b/src/Sms.TaskTwo.Wpf/App.xaml.cs @@ -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(); + MainWindow = new MainWindow(mainViewModel); + MainWindow.Show(); + } + + private void OnExit(object sender, ExitEventArgs e) + { + if (Services.GetService() 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(configuration); + services.AddSingleton(CreateEnvironmentStore); + services.AddTaskTwoCore(configuration); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private static IEnvironmentVariableStore CreateEnvironmentStore(IServiceProvider _) => + OperatingSystem.IsWindows() + ? new WindowsEnvironmentVariableStore() + : new LinuxEnvironmentVariableStore(); +} diff --git a/src/Sms.TaskTwo.Wpf/AppResources.cs b/src/Sms.TaskTwo.Wpf/AppResources.cs new file mode 100644 index 0000000..2d2dbab --- /dev/null +++ b/src/Sms.TaskTwo.Wpf/AppResources.cs @@ -0,0 +1,6 @@ +namespace Sms.TaskTwo.Wpf; + +public static class AppResources +{ + public const string WindowTitle = "Тестовое WPF-приложение для SmartMealService"; +} diff --git a/src/Sms.TaskTwo.Wpf/Converters/StringNotEmptyToVisibilityConverter.cs b/src/Sms.TaskTwo.Wpf/Converters/StringNotEmptyToVisibilityConverter.cs new file mode 100644 index 0000000..ab5b3e7 --- /dev/null +++ b/src/Sms.TaskTwo.Wpf/Converters/StringNotEmptyToVisibilityConverter.cs @@ -0,0 +1,16 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Sms.TaskTwo.Wpf.Converters; + +public sealed class StringNotEmptyToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value is string text && !string.IsNullOrEmpty(text) + ? Visibility.Visible + : Visibility.Collapsed; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} diff --git a/src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj b/src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj new file mode 100644 index 0000000..61bd3ac --- /dev/null +++ b/src/Sms.TaskTwo.Wpf/Sms.TaskTwo.Wpf.csproj @@ -0,0 +1,28 @@ + + + WinExe + net8.0-windows + enable + enable + true + app.manifest + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml b/src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml new file mode 100644 index 0000000..2cce772 --- /dev/null +++ b/src/Sms.TaskTwo.Wpf/Views/MainWindow.xaml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + +