From 747358297bdc5793de89eef30d5e7a8676033d4c Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 31 Aug 2025 14:25:27 +0200 Subject: [PATCH] Seperate database entity from ui object (Reactive) --- .../ConnectionStrings/ConnectionDescriptor.cs | 4 +- .../ServerConfigurationMappingTests.cs | 26 ++--- .../EditServerConfigurationWindowTests.cs | 6 +- pgLabII/Infra/LocalDb.cs | 9 +- pgLabII/Model/ServerConfiguration.cs | 103 ------------------ pgLabII/Model/ServerConfigurationEntity.cs | 20 ++++ .../Services/ServerConfigurationMapping.cs | 40 +++++-- .../EditServerConfigurationViewModel.cs | 19 ++-- .../ServerConfigurationViewModel.cs | 87 +++++++++++++++ pgLabII/ViewModels/ServerListViewModel.cs | 16 +-- 10 files changed, 176 insertions(+), 154 deletions(-) delete mode 100644 pgLabII/Model/ServerConfiguration.cs create mode 100644 pgLabII/Model/ServerConfigurationEntity.cs create mode 100644 pgLabII/ViewModels/ServerConfigurationViewModel.cs diff --git a/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs b/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs index f8dfe8c..3b39478 100644 --- a/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs +++ b/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs @@ -8,8 +8,6 @@ namespace pgLabII.PgUtils.ConnectionStrings; /// public sealed class ConnectionDescriptor { - public string? Name { get; init; } - // Primary hosts (support multi-host). If empty, implies localhost default. public IReadOnlyList Hosts { get; init; } = new List(); @@ -26,4 +24,4 @@ public sealed class ConnectionDescriptor // Additional parameters preserved across conversions public IReadOnlyDictionary Properties { get; init; } = new Dictionary(); -} \ No newline at end of file +} diff --git a/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs b/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs index 185f3cc..d30a894 100644 --- a/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs +++ b/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs @@ -12,20 +12,19 @@ public class ServerConfigurationMappingTests [Fact] public void ToDescriptor_Basic_MapsExpectedFields() { - var cfg = new ServerConfiguration + ServerConfigurationEntity cfg = new() { Name = "Prod", Host = "db.example.com", Port = 5433, InitialDatabase = "appdb", - DefaultSslMode = SslMode.Require, + SslMode = SslMode.Require, User = new ServerUser { Name = "alice", Password = "secret" } }; var extra = new Dictionary{{"search_path","public"}}; var d = ServerConfigurationMapping.ToDescriptor(cfg, applicationName: "pgLabII", timeoutSeconds: 15, extraProperties: extra); - Assert.Equal("Prod", d.Name); Assert.Single(d.Hosts); Assert.Equal("db.example.com", d.Hosts[0].Host); Assert.Equal((ushort)5433, d.Hosts[0].Port); @@ -42,7 +41,7 @@ public class ServerConfigurationMappingTests [Fact] public void ToDescriptor_OmitsEmptyFields() { - var cfg = new ServerConfiguration + ServerConfigurationEntity cfg = new () { Name = "Empty", Host = "", @@ -63,7 +62,6 @@ public class ServerConfigurationMappingTests { var desc = new ConnectionDescriptor { - Name = "Staging", Hosts = new [] { new HostEndpoint{ Host = "host1", Port = 5432 }, @@ -75,13 +73,12 @@ public class ServerConfigurationMappingTests SslMode = SslMode.VerifyFull }; - var cfg = ServerConfigurationMapping.FromDescriptor(desc); + ServerConfigurationEntity cfg = ServerConfigurationMapping.FromDescriptor(desc); - Assert.Equal("Staging", cfg.Name); Assert.Equal("host1", cfg.Host); Assert.Equal((ushort)5432, cfg.Port); Assert.Equal("stagedb", cfg.InitialDatabase); - Assert.Equal(SslMode.VerifyFull, cfg.DefaultSslMode); + Assert.Equal(SslMode.VerifyFull, cfg.SslMode); Assert.Equal("bob", cfg.User.Name); Assert.Equal("pwd", cfg.User.Password); } @@ -89,13 +86,13 @@ public class ServerConfigurationMappingTests [Fact] public void FromDescriptor_UpdatesExisting_PreservesMissing() { - var existing = new ServerConfiguration + ServerConfigurationEntity existing = new() { Name = "Existing", Host = "keep-host", Port = 5432, InitialDatabase = "keepdb", - DefaultSslMode = SslMode.Prefer, + SslMode = SslMode.Prefer, User = new ServerUser { Name = "keepuser", Password = "keeppwd" } }; @@ -110,7 +107,7 @@ public class ServerConfigurationMappingTests Assert.Equal("new-host", cfg.Host); Assert.Equal((ushort)5432, cfg.Port); // unchanged Assert.Equal("keepdb", cfg.InitialDatabase); // preserved - Assert.Equal(SslMode.Prefer, cfg.DefaultSslMode); // preserved + Assert.Equal(SslMode.Prefer, cfg.SslMode); // preserved Assert.Equal("keepuser", cfg.User.Name); // preserved Assert.Equal("keeppwd", cfg.User.Password); // preserved } @@ -118,24 +115,23 @@ public class ServerConfigurationMappingTests [Fact] public void Roundtrip_Basic() { - var cfg = new ServerConfiguration + ServerConfigurationEntity cfg = new() { Name = "Round", Host = "localhost", Port = 5432, InitialDatabase = "postgres", - DefaultSslMode = SslMode.Allow, + SslMode = SslMode.Allow, User = new ServerUser { Name = "me", Password = "pw" } }; var d = ServerConfigurationMapping.ToDescriptor(cfg); var cfg2 = ServerConfigurationMapping.FromDescriptor(d); - Assert.Equal(cfg.Name, cfg2.Name); Assert.Equal(cfg.Host, cfg2.Host); Assert.Equal(cfg.Port, cfg2.Port); Assert.Equal(cfg.InitialDatabase, cfg2.InitialDatabase); - Assert.Equal(cfg.DefaultSslMode, cfg2.DefaultSslMode); + Assert.Equal(cfg.SslMode, cfg2.SslMode); Assert.Equal(cfg.User.Name, cfg2.User.Name); Assert.Equal(cfg.User.Password, cfg2.User.Password); } diff --git a/pgLabII.Tests/Views/EditServerConfigurationWindowTests.cs b/pgLabII.Tests/Views/EditServerConfigurationWindowTests.cs index ee1552a..a68cce4 100644 --- a/pgLabII.Tests/Views/EditServerConfigurationWindowTests.cs +++ b/pgLabII.Tests/Views/EditServerConfigurationWindowTests.cs @@ -13,7 +13,7 @@ public class EditServerConfigurationWindowTests public void Parse_and_Generate_roundtrip_via_UI_bindings() { // Arrange: initialize Avalonia headless app once for the test - var vm = new EditServerConfigurationViewModel(new ServerConfiguration()); + var vm = new EditServerConfigurationViewModel(new ServerConfigurationEntity()); var window = new EditServerConfigurationWindow(vm); // Act: set an URL input, auto mode, then parse @@ -46,7 +46,7 @@ public class EditServerConfigurationWindowTests [AvaloniaFact] public void Forced_format_overrides_auto_detection() { - var vm = new EditServerConfigurationViewModel(new ServerConfiguration()); + var vm = new EditServerConfigurationViewModel(new ServerConfigurationEntity()); // Use a string with quoted values that libpq would struggle with due to incorrect quoting vm.InputConnectionString = "Host=\"server with spaces\";Username=\"bob\";Password=\"secret\";Database=\"db1\""; @@ -69,7 +69,7 @@ public class EditServerConfigurationWindowTests [AvaloniaFact] public void Parse_Npgsql_with_inline_host_port_updates_all_fields() { - var vm = new EditServerConfigurationViewModel(new ServerConfiguration()); + var vm = new EditServerConfigurationViewModel(new ServerConfigurationEntity()); vm.InputConnectionString = "Host=host.docker.internal:5432;Database=kms_quartz;Username=postgres;Password=admin;Trust Server Certificate=true"; vm.ForcedFormat = EditServerConfigurationViewModel.ForcedFormatOption.Auto; vm.ParseConnectionStringCommand.Execute().Subscribe(); diff --git a/pgLabII/Infra/LocalDb.cs b/pgLabII/Infra/LocalDb.cs index f665471..3c7dd9b 100644 --- a/pgLabII/Infra/LocalDb.cs +++ b/pgLabII/Infra/LocalDb.cs @@ -8,7 +8,7 @@ namespace pgLabII.Infra; public class LocalDb : DbContext { - public DbSet ServerConfigurations => Set(); + public DbSet ServerConfigurations => Set(); public DbSet Documents => Set(); public DbSet EditHistory => Set(); @@ -30,7 +30,7 @@ public class LocalDb : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { - new ServerConfigurationEntityConfiguration().Configure(modelBuilder.Entity()); + new ServerConfigurationEntityConfiguration().Configure(modelBuilder.Entity()); new ServerUserEntityConfiguration().Configure(modelBuilder.Entity()); new DocumentEntityConfiguration().Configure(modelBuilder.Entity()); new EditHistoryEntityConfiguration().Configure(modelBuilder.Entity()); @@ -40,15 +40,16 @@ public class LocalDb : DbContext { base.ConfigureConventions(configurationBuilder); + // Keep Color converter for any other entities still using Avalonia Color configurationBuilder .Properties() .HaveConversion(); } } -public class ServerConfigurationEntityConfiguration : IEntityTypeConfiguration +public class ServerConfigurationEntityConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder b) + public void Configure(EntityTypeBuilder b) { b.HasKey(e => e.Id); } diff --git a/pgLabII/Model/ServerConfiguration.cs b/pgLabII/Model/ServerConfiguration.cs deleted file mode 100644 index 756561e..0000000 --- a/pgLabII/Model/ServerConfiguration.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Reactive; -using Avalonia.Media; -using Npgsql; -using pgLabII.ViewModels; -using pgLabII.Views; -using ReactiveUI; -using ReactiveUI.SourceGenerators; - -namespace pgLabII.Model; - -public partial class ServerConfiguration : ReactiveObject -{ - private Color _color; - private bool _colorEnabled = true; - - [Reactive] private Guid _id = Guid.NewGuid(); - /// - /// For the user to help him identify the item - /// - [Reactive] private string _name = ""; - - public Color Color - { - get => _color; - set - { - if (_color != value) - { - _color = value; - this.RaisePropertyChanged(); - this.RaisePropertyChanged(propertyName: nameof(BackgroundBrush)); - } - } - } - - public bool ColorEnabled - { - get => _colorEnabled; - set - { - if (_colorEnabled != value) - { - _colorEnabled = value; - this.RaisePropertyChanged(); - this.RaisePropertyChanged(propertyName: nameof(BackgroundBrush)); - } - } - } - - [Reactive] private string _host = ""; - [Reactive] private ushort _port = 5432; - [Reactive] private string _initialDatabase = ""; - - [Reactive] private SslMode _sslMode = SslMode.Prefer; - - // Explicit property wrapper to make compiled XAML binding findable - public SslMode DefaultSslMode - { - get => _sslMode; - set => this.RaiseAndSetIfChanged(ref _sslMode, value); - } - - public IBrush? BackgroundBrush => ColorEnabled ? new SolidColorBrush(Color) : null; - - public ServerUser User { get; set; } = new(); - - public ReactiveCommand EditCommand { get; } - - public ReactiveCommand ExploreCommand { get; } - - public ServerConfiguration() - { - EditCommand = ReactiveCommand.Create(() => - { - EditServerConfigurationWindow window = new( - new ViewModels.EditServerConfigurationViewModel(this)) - { New = false }; - window.Show(); - }); - ExploreCommand = ReactiveCommand.Create(() => - { - SingleDatabaseWindow window = new() { DataContext = new ViewListViewModel() }; - window.Show(); - }); - } - - public ServerConfiguration(ServerConfiguration src) - : this() - { - Color = src.Color; - ColorEnabled = src.ColorEnabled; - Id = src.Id; - Name = src.Name; - Port = src.Port; - InitialDatabase = src.InitialDatabase; - DefaultSslMode = src.DefaultSslMode; - User = src.User; - } -} - diff --git a/pgLabII/Model/ServerConfigurationEntity.cs b/pgLabII/Model/ServerConfigurationEntity.cs new file mode 100644 index 0000000..425e920 --- /dev/null +++ b/pgLabII/Model/ServerConfigurationEntity.cs @@ -0,0 +1,20 @@ +using System; +using Npgsql; + +namespace pgLabII.Model; + +// Pure persistence entity for EF Core: no UI dependencies, no ReactiveObject +public class ServerConfigurationEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; + public string Host { get; set; } = string.Empty; + public ushort Port { get; set; } = 5432; + public string InitialDatabase { get; set; } = string.Empty; + public SslMode SslMode { get; set; } = SslMode.Prefer; + public bool ColorEnabled { get; set; } = true; + public int ColorArgb { get; set; } = unchecked((int)0xFF_33_33_33); // default dark gray + + public Guid UserId { get; set; } + public ServerUser User { get; set; } = new(); +} diff --git a/pgLabII/Services/ServerConfigurationMapping.cs b/pgLabII/Services/ServerConfigurationMapping.cs index 9f51efd..f82fc2d 100644 --- a/pgLabII/Services/ServerConfigurationMapping.cs +++ b/pgLabII/Services/ServerConfigurationMapping.cs @@ -21,7 +21,7 @@ public static class ServerConfigurationMapping /// - ApplicationName and TimeoutSeconds don't exist on ServerConfiguration; we preserve any passed-in /// values via optional parameters or Properties if provided by caller. /// - public static ConnectionDescriptor ToDescriptor(ServerConfiguration cfg, + public static ConnectionDescriptor ToDescriptor(ServerConfigurationEntity cfg, string? applicationName = null, int? timeoutSeconds = null, IReadOnlyDictionary? extraProperties = null) @@ -43,12 +43,11 @@ public static class ServerConfigurationMapping return new ConnectionDescriptor { - Name = cfg.Name, Hosts = hosts, Database = string.IsNullOrWhiteSpace(cfg.InitialDatabase) ? null : cfg.InitialDatabase, Username = string.IsNullOrWhiteSpace(cfg.User?.Name) ? null : cfg.User!.Name, Password = string.IsNullOrEmpty(cfg.User?.Password) ? null : cfg.User!.Password, - SslMode = cfg.DefaultSslMode, + SslMode = cfg.SslMode, ApplicationName = applicationName, TimeoutSeconds = timeoutSeconds, Properties = props @@ -61,17 +60,13 @@ public static class ServerConfigurationMapping /// - If descriptor has multiple hosts, the first is mapped to Host/Port. /// - If descriptor omits sslmode/database/username/password, existing values are preserved (if any). /// - public static ServerConfiguration FromDescriptor(ConnectionDescriptor descriptor, ServerConfiguration? existing = null) + public static ServerConfigurationEntity FromDescriptor(ConnectionDescriptor descriptor, ServerConfigurationEntity? existing = null) { if (descriptor == null) throw new ArgumentNullException(nameof(descriptor)); - var cfg = existing ?? new ServerConfiguration(); - - // Name - if (!string.IsNullOrWhiteSpace(descriptor.Name)) - cfg.Name = descriptor.Name!; + var cfg = existing ?? new ServerConfigurationEntity(); // Host/Port: take first - if (descriptor.Hosts != null && descriptor.Hosts.Count > 0) + if (descriptor.Hosts.Count > 0) { var h = descriptor.Hosts[0]; if (!string.IsNullOrWhiteSpace(h.Host)) @@ -86,9 +81,10 @@ public static class ServerConfigurationMapping // SSL Mode if (descriptor.SslMode.HasValue) - cfg.DefaultSslMode = descriptor.SslMode.Value; + cfg.SslMode = descriptor.SslMode.Value; // User + // Suspect, should the user object be replaced instead of updated? if (cfg.User == null) cfg.User = new ServerUser(); if (!string.IsNullOrWhiteSpace(descriptor.Username)) @@ -99,4 +95,26 @@ public static class ServerConfigurationMapping // Nothing to do for ApplicationName/TimeoutSeconds here; not represented in ServerConfiguration. return cfg; } + + // Overloads for new UI ViewModel wrapper + public static ConnectionDescriptor ToDescriptor(pgLabII.ViewModels.ServerConfigurationViewModel cfgVm, + string? applicationName = null, + int? timeoutSeconds = null, + IReadOnlyDictionary? extraProperties = null) + => ToDescriptor(cfgVm.Entity, applicationName, timeoutSeconds, extraProperties); + + public static void FromDescriptorInto(pgLabII.ViewModels.ServerConfigurationViewModel targetVm, ConnectionDescriptor descriptor) + { + //var updated = targetVm.Entity; + var n = FromDescriptor(descriptor, null); + // push back updated values into VM's entity to trigger the notifies + targetVm.Host = n.Host; + targetVm.Port = n.Port; + targetVm.InitialDatabase = n.InitialDatabase; + targetVm.DefaultSslMode = n.SslMode; + // Suspect, if we share Users between configurations then we should not be overriding this struct + if (targetVm.User == null) targetVm.User = new ServerUser(); + targetVm.User.Name = n.User?.Name ?? string.Empty; + targetVm.User.Password = n.User?.Password ?? string.Empty; + } } diff --git a/pgLabII/ViewModels/EditServerConfigurationViewModel.cs b/pgLabII/ViewModels/EditServerConfigurationViewModel.cs index 4351a45..8ce83ac 100644 --- a/pgLabII/ViewModels/EditServerConfigurationViewModel.cs +++ b/pgLabII/ViewModels/EditServerConfigurationViewModel.cs @@ -12,7 +12,8 @@ namespace pgLabII.ViewModels; public class EditServerConfigurationViewModel : ViewModelBase { - public ServerConfiguration Configuration { get; set; } + // Prefer new UI VM; keep old model for compatibility by wrapping when needed + public ServerConfigurationViewModel Configuration { get; set; } // Connection string IO private string _inputConnectionString = string.Empty; @@ -86,7 +87,7 @@ public class EditServerConfigurationViewModel : ViewModelBase public EditServerConfigurationViewModel() { - Configuration = new(); + Configuration = new(new ServerConfigurationEntity()); _service = ConnectionStringService.CreateDefault(); ParseConnectionStringCommand = ReactiveCommand.Create(ParseConnectionString); @@ -97,10 +98,10 @@ public class EditServerConfigurationViewModel : ViewModelBase CloseCommand = ReactiveCommand.Create(() => { }); } - public EditServerConfigurationViewModel(ServerConfiguration configuration) + public EditServerConfigurationViewModel(ServerConfigurationEntity configuration) : this() { - Configuration = configuration; + Configuration = new ServerConfigurationViewModel(configuration); } private void DetectFormat() @@ -131,19 +132,21 @@ public class EditServerConfigurationViewModel : ViewModelBase _ => new UrlCodec() }; var r = codec.TryParse(InputConnectionString); - if (r.IsSuccess) descriptor = r.Value; + if (r.IsSuccess) + descriptor = r.Value; } else { var r = _service.ParseToDescriptor(InputConnectionString); - if (r.IsSuccess) descriptor = r.Value; + if (r.IsSuccess) + descriptor = r.Value; } if (descriptor != null) { // Map into our configuration (update existing) - ServerConfigurationMapping.FromDescriptor(descriptor, Configuration); - // Also set sensible default OutputFormat to the detected/forced one + ServerConfigurationMapping.FromDescriptorInto(Configuration, descriptor); + // Also set a sensible default OutputFormat to the detected/forced one if (forced == ForcedFormatOption.Auto) { if (DetectedFormat.HasValue) OutputFormat = DetectedFormat.Value; diff --git a/pgLabII/ViewModels/ServerConfigurationViewModel.cs b/pgLabII/ViewModels/ServerConfigurationViewModel.cs new file mode 100644 index 0000000..3fa702a --- /dev/null +++ b/pgLabII/ViewModels/ServerConfigurationViewModel.cs @@ -0,0 +1,87 @@ +using System; +using System.Reactive; +using Avalonia.Media; +using Npgsql; +using pgLabII.Model; +using ReactiveUI; + +namespace pgLabII.ViewModels; + +// UI ViewModel that wraps the persistence entity +public class ServerConfigurationViewModel : ReactiveObject +{ + private readonly ServerConfigurationEntity _entity; + + public ServerConfigurationViewModel(ServerConfigurationEntity entity) + { + _entity = entity ?? throw new ArgumentNullException(nameof(entity)); + EditCommand = ReactiveCommand.Create(() => + { + var vm = new EditServerConfigurationViewModel(_entity); + var window = new pgLabII.Views.EditServerConfigurationWindow(vm) { New = false }; + window.Show(); + }); + ExploreCommand = ReactiveCommand.Create(() => { /* window coordination can be injected later */ }); + } + + public ServerConfigurationEntity Entity => _entity; + + public Guid Id + { + get => _entity.Id; + set { if (_entity.Id != value) { _entity.Id = value; this.RaisePropertyChanged(); } } + } + + public string Name + { + get => _entity.Name; + set { if (_entity.Name != value) { _entity.Name = value; this.RaisePropertyChanged(); } } + } + + public string Host + { + get => _entity.Host; + set { if (_entity.Host != value) { _entity.Host = value; this.RaisePropertyChanged(); } } + } + + public ushort Port + { + get => _entity.Port; + set { if (_entity.Port != value) { _entity.Port = value; this.RaisePropertyChanged(); } } + } + + public string InitialDatabase + { + get => _entity.InitialDatabase; + set { if (_entity.InitialDatabase != value) { _entity.InitialDatabase = value; this.RaisePropertyChanged(); } } + } + + public SslMode DefaultSslMode + { + get => _entity.SslMode; + set { if (_entity.SslMode != value) { _entity.SslMode = value; this.RaisePropertyChanged(); } } + } + + public bool ColorEnabled + { + get => _entity.ColorEnabled; + set { if (_entity.ColorEnabled != value) { _entity.ColorEnabled = value; this.RaisePropertyChanged(); this.RaisePropertyChanged(nameof(BackgroundBrush)); } } + } + + public Color Color + { + get => Color.FromUInt32((uint)_entity.ColorArgb); + set { var argb = unchecked((int)value.ToUInt32()); if (_entity.ColorArgb != argb) { _entity.ColorArgb = argb; this.RaisePropertyChanged(); this.RaisePropertyChanged(nameof(BackgroundBrush)); } } + } + + public IBrush? BackgroundBrush => ColorEnabled ? new SolidColorBrush(Color) : null; + + public ServerUser User + { + get => _entity.User; + set { if (!ReferenceEquals(_entity.User, value)) { _entity.User = value; this.RaisePropertyChanged(); } } + } + + public ReactiveCommand EditCommand { get; } + public ReactiveCommand ExploreCommand { get; } +} diff --git a/pgLabII/ViewModels/ServerListViewModel.cs b/pgLabII/ViewModels/ServerListViewModel.cs index a017016..6a44a38 100644 --- a/pgLabII/ViewModels/ServerListViewModel.cs +++ b/pgLabII/ViewModels/ServerListViewModel.cs @@ -9,12 +9,11 @@ namespace pgLabII.ViewModels; public class ServerListViewModel : ViewModelBase { - public ObservableCollection ServerConfigurations { get; } = + public ObservableCollection ServerConfigurations { get; } = [ - new ServerConfiguration() + new (new() { Name = "Local pg15", - Color = Colors.Aquamarine, ColorEnabled = true, Host = "localhost", Port = 5434, @@ -24,22 +23,25 @@ public class ServerListViewModel : ViewModelBase Name = "postgres", Password = "admin", }, + }) + { + Color = Colors.Aquamarine, }, - new ServerConfiguration() + new (new () { Name = "Bar", ColorEnabled = false, Host = "db.host.nl" - } + }), ]; - public ReactiveCommand RemoveServerCommand { get; } + public ReactiveCommand RemoveServerCommand { get; } public ReactiveCommand AddServerCommand { get; } public ServerListViewModel() { - RemoveServerCommand = ReactiveCommand.Create((sc) => + RemoveServerCommand = ReactiveCommand.Create((sc) => { ServerConfigurations.Remove(sc); return Unit.Default;