diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml index a7da255..1f056bf 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml @@ -14,15 +14,26 @@ - + - - - + + + + + + + + + + + + + + public sealed partial class ServiceDetailsWindow : Window { - public AgentClient Agent { get; } + public AgentClient Client { get; } public ServiceInfo Service { get; } private CancellationTokenSource? _cts; @@ -37,7 +40,7 @@ public sealed partial class ServiceDetailsWindow : Window public ServiceDetailsWindow(AgentClient agentClient, ServiceInfo serviceInfo) { InitializeComponent(); - Agent = agentClient; + Client = agentClient; Service = serviceInfo; ExtendsContentIntoTitleBar = true; @@ -50,15 +53,182 @@ public ServiceDetailsWindow(AgentClient agentClient, ServiceInfo serviceInfo) this.Closed += (_, _) => _cts?.Cancel(); } + private async void ServiceStartMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + ServiceInfo serviceInfo = Service; + + UnitActionReply response = await Client!.Service.PerformUnitActionAsync(new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Start + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully started the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "started"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + + private async void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + + ServiceInfo serviceInfo = (ServiceInfo)menuFlyoutItem.DataContext; + + ContentDialog dialog = new() + { + XamlRoot = this.Content.XamlRoot, + Title = $"Are you sure you want to Stop {serviceInfo.Name}?", + CloseButtonText = "Cancel", + PrimaryButtonText = "Stop", + }; + + ContentDialogResult result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + UnitActionReply response = await Client!.Service.PerformUnitActionAsync(request: new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Stop + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully stopped the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "stopped"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + else + { + // Cancel, do nothing + } + + } + + private async void ServiceRestartMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + + ServiceInfo serviceInfo = Service; + + ContentDialog dialog = new() + { + XamlRoot = this.Content.XamlRoot, + Title = $"Are you sure you want to Restart {serviceInfo.Name}?", + CloseButtonText = "Cancel", + PrimaryButtonText = "Restart", + }; + + ContentDialogResult result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + try + { + UnitActionReply response = await Client!.Service.PerformUnitActionAsync(new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Restart + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "restarted"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + catch (Exception ex) + { + if (ex is Grpc.Core.RpcException && serviceInfo.Name == "agent.service") + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the Agent!", InfoBarSeverity.Success); + } + else + { + await ShowErrorInfoBarAsync($"Exception during restart: {ex.Message}"); + } + } + } + } + + private async void ServiceEnableMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + ServiceInfo serviceInfo = Service; + + UnitFileActionReply response = await Client!.Service.PerformUnitFileActionAsync(new UnitFileActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitFileActionRequest.Types.UnitFileAction.Enable + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully enabled the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "enabled"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + + private async void ServiceDisableMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + ServiceInfo serviceInfo = Service; + + UnitFileActionReply response = await Client!.Service.PerformUnitFileActionAsync(new UnitFileActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitFileActionRequest.Types.UnitFileAction.Disable + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully disabled the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "disabled"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + private async Task GetLogsAsync(CancellationToken cancellationToken) { - if (Agent == null) return; + if (Client == null) return; AsyncServerStreamingCall? call = null; try { - call = Agent.Journal.Action(new Journal.V1.JournalRequest() + call = Client.Journal.Action(new Journal.V1.JournalRequest() { NumFromTail = 50, Field = Journal.V1.JournalRequest.Types.Field.Systemd, @@ -152,5 +322,81 @@ private async Task GetLogsAsync(CancellationToken cancellationToken) } } } + + private async Task UpdateServiceVisualStateAsync(ServiceInfo serviceInfo, string action) + { + await DispatcherQueue.EnqueueAsync(() => + { + switch (action) + { + case "enabled": + serviceInfo.State = "enabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"]; + break; + case "disabled": + serviceInfo.State = "disabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + case "started": + serviceInfo.ActiveState = "started"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"]; + break; + case "restarted": + serviceInfo.ActiveState = "restarted"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"]; + break; + case "stopped": + serviceInfo.ActiveState = "stopped"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + default: + break; + } + }); + } + + private async Task ShowInfoBarAsync(string title, string message, InfoBarSeverity severity) + { + // Ensure UI thread + await DispatcherQueue.EnqueueAsync(() => + { + if (GridRoot == null) + { + // fallback to page XamlRoot; but InfoBar must be in visual tree to be visible + // if GridRoot is not available the InfoBar won't be shown persistently. + return; + } + + InfoBar infoBar = new() + { + Title = title, + Message = message, + Severity = severity, + IsOpen = true, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(0, 0, 0, 0) + }; + + // Add to visual tree so it is visible + GridRoot.Children.Add(infoBar); + + // Remove from visual tree when closed + void OnClosed(object? s, InfoBarClosedEventArgs args) + { + infoBar.Closed -= OnClosed; + if (GridRoot.Children.Contains(infoBar)) + { + GridRoot.Children.Remove(infoBar); + } + } + + infoBar.Closed += OnClosed; + }); + } + + private async Task ShowErrorInfoBarAsync(string message) + { + await ShowInfoBarAsync("Service Action Result", $"Error: {message}", InfoBarSeverity.Error); + } } } diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml index 7dbffea..a225039 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml @@ -49,10 +49,11 @@ ShowsScrollingPlaceholders="True" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" - Grid.Row="1"> + Grid.Row="1" + > - + @@ -66,33 +67,53 @@ - + - - - + + + + + + + - - + + + + - - + + Margin="0,0,0,8"/> + + + + + + + + + - + diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs index cf31348..c68ca42 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs @@ -1,5 +1,6 @@ using Grpc.Core; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -119,9 +120,9 @@ private async void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e) ContentDialog dialog = new() { XamlRoot = this.XamlRoot, - Title = $"Are you sure you want to Stop {serviceInfo.Name} ?", + Title = $"Are you sure you want to Stop {serviceInfo.Name}?", CloseButtonText = "Cancel", - PrimaryButtonText = "Kill", + PrimaryButtonText = "Stop", }; ContentDialogResult result = await dialog.ShowAsync(); @@ -143,7 +144,8 @@ private async void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e) { await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); } - } else + } + else { // Cancel, do nothing } @@ -156,33 +158,46 @@ private async void ServiceRestartMenuItem_Click(object sender, RoutedEventArgs e ServiceInfo serviceInfo = (ServiceInfo)menuFlyoutItem.DataContext; - try + ContentDialog dialog = new() { - UnitActionReply response = await client!.Service.PerformUnitActionAsync(new UnitActionRequest - { - UnitName = serviceInfo.Name, - Action = UnitActionRequest.Types.UnitAction.Restart - }); + XamlRoot = this.XamlRoot, + Title = $"Are you sure you want to Restart {serviceInfo.Name}?", + CloseButtonText = "Cancel", + PrimaryButtonText = "Restart", + }; - if (response.Success) - { - await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the service!", InfoBarSeverity.Success); - await UpdateServiceVisualStateAsync(serviceInfo, "restarted"); - } - else - { - await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); - } - } - catch (Exception ex) + ContentDialogResult result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) { - if (ex is Grpc.Core.RpcException && serviceInfo.Name == "agent.service") + try { - await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the Agent!", InfoBarSeverity.Success); + UnitActionReply response = await client!.Service.PerformUnitActionAsync(new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Restart + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "restarted"); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } } - else + catch (Exception ex) { - await ShowErrorInfoBarAsync($"Exception during restart: {ex.Message}"); + if (ex is Grpc.Core.RpcException && serviceInfo.Name == "agent.service") + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the Agent!", InfoBarSeverity.Success); + } + else + { + await ShowErrorInfoBarAsync($"Exception during restart: {ex.Message}"); + } } } } @@ -323,7 +338,6 @@ private async Task InitializeAsync(NavigationEventArgs e, System.Threading.Cance await DispatcherQueue.EnqueueAsync(() => { services.Clear(); - services.Add(new ServiceInfo { Name = "(error)", Description = ex.Message, Fill = new SolidColorBrush(Colors.Red) }); }).ConfigureAwait(false); await ShowErrorInfoBarAsync($"Initialization failed: {ex.Message}"); } @@ -332,7 +346,7 @@ await DispatcherQueue.EnqueueAsync(() => await DispatcherQueue.EnqueueAsync(() => { services.Clear(); - services.Add(new ServiceInfo { Name = "(error)", Description = ex.Message, Fill = new SolidColorBrush(Colors.Red) }); + services.Add(new ServiceInfo { Name = "(error)", Description = ex.Message, StateFill = new SolidColorBrush(Colors.Red) }); }).ConfigureAwait(false); await ShowErrorInfoBarAsync($"Initialization failed: {ex.Message}"); @@ -368,33 +382,64 @@ private async Task LoadAllServices(CancellationToken cancellationToken = default return; } - GetUnitsReply? response = await client.Service.GetAllUnitsAsync(request: new GetUnitsRequest(), cancellationToken: cancellationToken); - if (response is null) - { - await ShowErrorInfoBarAsync("Failed to retrieve services: received null response from agent."); - return; - } + GetUnitsReply? response = await client.Service.GetAllUnitsAsync(request: new GetUnitsRequest(), cancellationToken: cancellationToken); + GetUnitsReply? loadedResponse = await client.Service.GetLoadedUnitsAsync(request: new GetUnitsRequest(), cancellationToken: cancellationToken); + if (response is null || loadedResponse is null) + { + await ShowErrorInfoBarAsync("Failed to retrieve services: received null response from agent."); + return; + } - foreach (LoadedUnit? unit in response.Units) + IEnumerable units = response.Units; + IEnumerable loadedUnits = loadedResponse.Units; + + // Returns the union of both, updating LoadState where possible + units = units.Join( + loadedUnits, + unit => ExtractShortUnitName(unit.Name), + loadedUnit => loadedUnit.Name, + (unit, loadedUnit) => { - string unitName = ExtractShortUnitName(unit.Name); - // Choose a color/brush based on unit state - string state = (unit.LoadState ?? string.Empty).ToLowerInvariant(); - SolidColorBrush brush = state switch - { - "enabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], - "loaded" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], - "static" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], - "disabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], - _ => new SolidColorBrush(Colors.Goldenrod) - }; - - ServiceInfo serviceInfo = new ServiceInfo + if (loadedUnit is not null) { - Name = unitName, - Description = $"State: {unit.LoadState}", - Fill = brush - }; + loadedUnit.LoadState = unit.LoadState; + return loadedUnit; + } + return unit; + }); + + foreach (LoadedUnit? unit in units) + { + string unitName = ExtractShortUnitName(unit.Name); + // Choose a color/brush based on unit state + string state = (unit.LoadState ?? string.Empty).ToLowerInvariant(); + SolidColorBrush brush = state switch + { + "enabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], + "loaded" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], + "static" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], + "disabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], + _ => new SolidColorBrush(Colors.Goldenrod) + }; + + SolidColorBrush activeStateFill = unit.ActiveState switch + { + "running" => (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"], + "exited" => (SolidColorBrush)Application.Current.Resources["SystemFillColorNeutralBrush"], + "dead" => (SolidColorBrush)Application.Current.Resources["SystemFillColorNeutralBrush"], + "failed" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBackgroundBrush"], + _ => new SolidColorBrush(Colors.Goldenrod) + }; + + ServiceInfo serviceInfo = new ServiceInfo + { + Name = unitName, + Description = unit.Description ?? unitName, + State = unit.LoadState ?? "Unknown", + ActiveState = unit.ActiveState ?? "", + StateFill = brush, + ActiveStateFill = activeStateFill + }; await DispatcherQueue.EnqueueAsync(() => { @@ -461,28 +506,31 @@ private async Task UpdateServiceVisualStateAsync(ServiceInfo serviceInfo, string { await DispatcherQueue.EnqueueAsync(() => { - string desc = action switch - { - "started" => "State: running", - "stopped" => "State: stopped", - "restarted" => "State: running", - "enabled" => "State: enabled", - "disabled" => "State: disabled", - _ => serviceInfo.Description - }; - - SolidColorBrush brush = action switch + switch(action) { - "started" => new SolidColorBrush(Colors.Green), - "stopped" => new SolidColorBrush(Colors.Gray), - "restarted" => new SolidColorBrush(Colors.Green), - "enabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], - "disabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], - _ => serviceInfo.Fill - }; - - serviceInfo.Description = desc; - serviceInfo.Fill = brush; + case "enabled": + serviceInfo.State = "enabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"]; + break; + case "disabled": + serviceInfo.State = "disabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + case "started": + serviceInfo.ActiveState = "started"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"]; + break; + case "restarted": + serviceInfo.ActiveState = "restarted"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"]; + break; + case "stopped": + serviceInfo.ActiveState = "stopped"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + default: + break; + } }); } @@ -498,17 +546,17 @@ private void Order_Services() List ordered = order switch { "Name" => services.OrderBy(s => s.Name).ThenBy(s => s.Description).ToList(), - "State" => services.OrderBy(p => p.Description).ThenBy(s => s.Name).ToList(), + "State" => services.OrderBy(p => p.State).ThenBy(s => s.Name).ToList(), _ => services .OrderBy(s => { - if (!string.IsNullOrEmpty(s.Description) && s.Description.Contains("State: enabled", StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty(s.State) && s.State.Contains("enabled", StringComparison.InvariantCultureIgnoreCase)) return 0; - if (!string.IsNullOrEmpty(s.Description) && s.Description.Contains("State: disabled", StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty(s.State) && s.State.Contains("disabled", StringComparison.InvariantCultureIgnoreCase)) return 1; return 2; }) - .ThenBy(s => s.Description, StringComparer.InvariantCultureIgnoreCase) + .ThenBy(s => s.State, StringComparer.InvariantCultureIgnoreCase) .ThenBy(s => s.Name, StringComparer.InvariantCultureIgnoreCase) .ToList() }; @@ -592,52 +640,94 @@ await DispatcherQueue.EnqueueAsync(() => } } } - - // Small view-model used by the DataTemplate in XAML. - public sealed class ServiceInfo : INotifyPropertyChanged + + // Small view-model used by the DataTemplate in XAML. + public sealed class ServiceInfo : INotifyPropertyChanged + { + private string _name = string.Empty; + public string Name { - private string _name = string.Empty; - public string Name + get => _name; + set { - get => _name; - set + if (_name != value) { - if (_name != value) - { - _name = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); - } + _name = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } + } - private string _description = string.Empty; - public string Description + private string _description = string.Empty; + public string Description + { + get => _description; + set { - get => _description; - set + if (_description != value) { - if (_description != value) - { - _description = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description))); - } + _description = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description))); + } + } + } + + private string _state = string.Empty; + public string State + { + get => _state; + set + { + if (_state != value) + { + _state = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); } } + } - private SolidColorBrush _fill = new SolidColorBrush(Colors.Transparent); - public SolidColorBrush Fill + private string _activeState = string.Empty; + public string ActiveState + { + get => _activeState; + set { - get => _fill; - set + if (_activeState != value) { - if (_fill != value) - { - _fill = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Fill))); - } + _activeState = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ActiveState))); + } + } + } + + private SolidColorBrush _stateFill = new SolidColorBrush(Colors.Transparent); + public SolidColorBrush StateFill + { + get => _stateFill; + set + { + if (_stateFill != value) + { + _stateFill = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateFill))); } } + } - public event PropertyChangedEventHandler? PropertyChanged; + private SolidColorBrush _activeStateFill = new SolidColorBrush(Colors.Transparent); + public SolidColorBrush ActiveStateFill + { + get => _activeStateFill; + set + { + if (_activeStateFill != value) + { + _activeStateFill = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ActiveStateFill))); + } + } } + + public event PropertyChangedEventHandler? PropertyChanged; } +}