diff --git a/PettingZoo.Core/Connection/IConnection.cs b/PettingZoo.Core/Connection/IConnection.cs index 4f4d0da..f4ac798 100644 --- a/PettingZoo.Core/Connection/IConnection.cs +++ b/PettingZoo.Core/Connection/IConnection.cs @@ -8,6 +8,8 @@ namespace PettingZoo.Core.Connection event EventHandler StatusChanged; ISubscriber Subscribe(string exchange, string routingKey); + ISubscriber Subscribe(); + Task Publish(PublishMessageInfo messageInfo); } diff --git a/PettingZoo.Core/Connection/ISubscriber.cs b/PettingZoo.Core/Connection/ISubscriber.cs index 0148cc4..9beb336 100644 --- a/PettingZoo.Core/Connection/ISubscriber.cs +++ b/PettingZoo.Core/Connection/ISubscriber.cs @@ -4,8 +4,9 @@ namespace PettingZoo.Core.Connection { public interface ISubscriber : IAsyncDisposable { - string Exchange {get; } - string RoutingKey { get; } + string? QueueName { get; } + string? Exchange {get; } + string? RoutingKey { get; } event EventHandler? MessageReceived; diff --git a/PettingZoo.Core/Settings/IConnectionSettingsRepository.cs b/PettingZoo.Core/Settings/IConnectionSettingsRepository.cs new file mode 100644 index 0000000..18dd443 --- /dev/null +++ b/PettingZoo.Core/Settings/IConnectionSettingsRepository.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PettingZoo.Core.Settings +{ + public interface IConnectionSettingsRepository + { + Task GetLastUsed(); + Task StoreLastUsed(ConnectionSettings connectionSettings); + + Task> GetStored(); + Task Add(string displayName, ConnectionSettings connectionSettings); + Task Update(Guid id, string displayName, ConnectionSettings connectionSettings); + Task Delete(Guid id); + } + + + public class ConnectionSettings + { + public string Host { get; } + public string VirtualHost { get; } + public int Port { get; } + public string Username { get; } + public string? Password { get; } + + public bool Subscribe { get; } + public string Exchange { get; } + public string RoutingKey { get; } + + + public static readonly ConnectionSettings Default = new("localhost", "/", 5672, "guest", "guest", false, "", "#"); + + + public ConnectionSettings(string host, string virtualHost, int port, string username, string? password, + bool subscribe, string exchange, string routingKey) + { + Host = host; + VirtualHost = virtualHost; + Port = port; + Username = username; + Password = password; + + Subscribe = subscribe; + Exchange = exchange; + RoutingKey = routingKey; + } + + + public bool SameParameters(ConnectionSettings value, bool comparePassword = true) + { + return Host == value.Host && + VirtualHost == value.VirtualHost && + Port == value.Port && + Username == value.Username && + (!comparePassword || Password == value.Password) && + Subscribe == value.Subscribe && + Exchange == value.Exchange && + RoutingKey == value.RoutingKey; + } + } + + + public class StoredConnectionSettings : ConnectionSettings + { + public Guid Id { get; } + public string DisplayName { get; } + + public StoredConnectionSettings(Guid id, string displayName, string host, string virtualHost, int port, string username, + string? password, bool subscribe, string exchange, string routingKey) + : base(host, virtualHost, port, username, password, subscribe, exchange, routingKey) + { + Id = id; + DisplayName = displayName; + } + + + public StoredConnectionSettings(Guid id, string displayName, ConnectionSettings connectionSettings) + : base(connectionSettings.Host, connectionSettings.VirtualHost, connectionSettings.Port, connectionSettings.Username, connectionSettings.Password, + connectionSettings.Subscribe, connectionSettings.Exchange, connectionSettings.RoutingKey) + { + Id = id; + DisplayName = displayName; + } + } +} \ No newline at end of file diff --git a/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs index a6f54e5..9f790c8 100644 --- a/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs +++ b/PettingZoo.RabbitMQ/RabbitMQClientConnection.cs @@ -46,18 +46,32 @@ namespace PettingZoo.RabbitMQ connection = null; } } + + GC.SuppressFinalize(this); } public ISubscriber Subscribe(string exchange, string routingKey) + { + return CreateSubscriber(exchange, routingKey); + } + + + public ISubscriber Subscribe() + { + return CreateSubscriber(null, null); + } + + + private ISubscriber CreateSubscriber(string? exchange, string? routingKey) { lock (connectionLock) { var subscriber = new RabbitMQClientSubscriber(model, exchange, routingKey); - if (model != null) + if (model != null) return subscriber; - + void ConnectSubscriber(object? sender, StatusChangedEventArgs args) { if (args.Status != ConnectionStatus.Connected) @@ -67,20 +81,20 @@ namespace PettingZoo.RabbitMQ { if (model == null) return; - + subscriber.Connected(model); } - + StatusChanged -= ConnectSubscriber; } - + StatusChanged += ConnectSubscriber; return subscriber; } } - + public Task Publish(PublishMessageInfo messageInfo) { if (model == null) diff --git a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs index bc07d37..2c24db9 100644 --- a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs +++ b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs @@ -13,12 +13,13 @@ namespace PettingZoo.RabbitMQ private string? consumerTag; private bool started; - public string Exchange { get; } - public string RoutingKey { get; } + public string? QueueName { get; private set; } + public string? Exchange { get; } + public string? RoutingKey { get; } public event EventHandler? MessageReceived; - public RabbitMQClientSubscriber(IModel? model, string exchange, string routingKey) + public RabbitMQClientSubscriber(IModel? model, string? exchange, string? routingKey) { this.model = model; Exchange = exchange; @@ -28,6 +29,8 @@ namespace PettingZoo.RabbitMQ public ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + if (model != null && consumerTag != null && model.IsOpen) model.BasicCancelNoWait(consumerTag); @@ -41,13 +44,14 @@ namespace PettingZoo.RabbitMQ if (model == null) return; - var queueName = model.QueueDeclare().QueueName; - model.QueueBind(queueName, Exchange, RoutingKey); + QueueName = model.QueueDeclare().QueueName; + if (Exchange != null && RoutingKey != null) + model.QueueBind(QueueName, Exchange, RoutingKey); var consumer = new EventingBasicConsumer(model); consumer.Received += ClientReceived; - consumerTag = model.BasicConsume(queueName, true, consumer); + consumerTag = model.BasicConsume(QueueName, true, consumer); } diff --git a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs new file mode 100644 index 0000000..a24bc13 --- /dev/null +++ b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs @@ -0,0 +1,30 @@ +using LiteDB; +using LiteDB.Async; + +namespace PettingZoo.Settings.LiteDB +{ + public class BaseLiteDBRepository + { + private readonly string databaseFilename; + + + public BaseLiteDBRepository(string databaseName) + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (appDataPath == null) + throw new IOException("Could not resolve application data path"); + + var databasePath = Path.Combine(appDataPath, @"PettingZoo"); + if (!Directory.CreateDirectory(databasePath).Exists) + throw new IOException($"Failed to create directory: {databasePath}"); + + databaseFilename = Path.Combine(databasePath, $"{databaseName}.litedb"); + } + + + protected ILiteDatabaseAsync GetDatabase() + { + return new LiteDatabaseAsync(databaseFilename); + } + } +} diff --git a/PettingZoo.Settings.LiteDB/LiteDBConnectionSettingsRepository.cs b/PettingZoo.Settings.LiteDB/LiteDBConnectionSettingsRepository.cs new file mode 100644 index 0000000..dde8c63 --- /dev/null +++ b/PettingZoo.Settings.LiteDB/LiteDBConnectionSettingsRepository.cs @@ -0,0 +1,130 @@ +using PettingZoo.Core.Settings; + +namespace PettingZoo.Settings.LiteDB +{ + public class LiteDBConnectionSettingsRepository : BaseLiteDBRepository, IConnectionSettingsRepository + { + private static readonly Guid LastUsedId = new("1624147f-76b2-4b5e-8e6f-2ef1730a0a99"); + + private const string CollectionLastUsed = "lastUsed"; + private const string CollectionStored = "stored"; + + + public LiteDBConnectionSettingsRepository() : base(@"connectionSettings") + { + } + + + public async Task GetLastUsed() + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionLastUsed); + + var lastUsed = await collection.FindOneAsync(r => true); + if (lastUsed == null) + return ConnectionSettings.Default; + + return new ConnectionSettings( + lastUsed.Host, + lastUsed.VirtualHost, + lastUsed.Port, + lastUsed.Username, + lastUsed.Password, + lastUsed.Subscribe, + lastUsed.Exchange, + lastUsed.RoutingKey); + } + + + public async Task StoreLastUsed(ConnectionSettings connectionSettings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionLastUsed); + + await collection.UpsertAsync(ConnectionSettingsRecord.FromConnectionSettings(LastUsedId, connectionSettings, "")); + } + + + public async Task> GetStored() + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + return (await collection.FindAllAsync()) + .Select(r => new StoredConnectionSettings(r.Id, r.DisplayName, r.Host, r.VirtualHost, r.Port, r.Username, r.Password, r.Subscribe, r.Exchange, r.RoutingKey)) + .ToArray(); + } + + + public async Task Add(string displayName, ConnectionSettings connectionSettings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + var id = Guid.NewGuid(); + await collection.InsertAsync(ConnectionSettingsRecord.FromConnectionSettings(id, connectionSettings, displayName)); + + return new StoredConnectionSettings(id, displayName, connectionSettings); + } + + + public async Task Update(Guid id, string displayName, ConnectionSettings connectionSettings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + await collection.UpdateAsync(ConnectionSettingsRecord.FromConnectionSettings(id, connectionSettings, displayName)); + return new StoredConnectionSettings(id, displayName, connectionSettings); + } + + + public async Task Delete(Guid id) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionStored); + + await collection.DeleteAsync(id); + } + + + // ReSharper disable MemberCanBePrivate.Local - for LiteDB + // ReSharper disable PropertyCanBeMadeInitOnly.Local + private class ConnectionSettingsRecord + { + public Guid Id { get; set; } + + public string DisplayName { get; set; } = null!; + public string Host { get; set; } = null!; + public string VirtualHost { get; set; } = null!; + public int Port { get; set; } + public string Username { get; set; } = null!; + public string? Password { get; set; } + + public bool Subscribe { get; set; } + public string Exchange { get; set; } = null!; + public string RoutingKey { get; set; } = null!; + + + public static ConnectionSettingsRecord FromConnectionSettings(Guid id, ConnectionSettings connectionSettings, string displayName) + { + return new ConnectionSettingsRecord + { + Id = id, + DisplayName = displayName, + + Host = connectionSettings.Host, + VirtualHost = connectionSettings.VirtualHost, + Port = connectionSettings.Port, + Username = connectionSettings.Username, + Password = connectionSettings.Password, + + Subscribe = connectionSettings.Subscribe, + Exchange = connectionSettings.Exchange, + RoutingKey = connectionSettings.RoutingKey + }; + } + } + // ReSharper restore PropertyCanBeMadeInitOnly.Local + // ReSharper restore MemberCanBePrivate.Local + } +} \ No newline at end of file diff --git a/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj b/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj new file mode 100644 index 0000000..d6532ad --- /dev/null +++ b/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/PettingZoo.sln b/PettingZoo.sln index a8028f7..66d5304 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -14,7 +14,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Tapeti", "PettingZoo.Tapeti\PettingZoo.Tapeti.csproj", "{1763AB04-59D9-4663-B207-D6302FFAACD5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Tapeti", "PettingZoo.Tapeti\PettingZoo.Tapeti.csproj", "{1763AB04-59D9-4663-B207-D6302FFAACD5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -38,6 +40,10 @@ Global {1763AB04-59D9-4663-B207-D6302FFAACD5}.Debug|Any CPU.Build.0 = Debug|Any CPU {1763AB04-59D9-4663-B207-D6302FFAACD5}.Release|Any CPU.ActiveCfg = Release|Any CPU {1763AB04-59D9-4663-B207-D6302FFAACD5}.Release|Any CPU.Build.0 = Release|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PettingZoo.sln.DotSettings b/PettingZoo.sln.DotSettings index f8f70cf..0b00c05 100644 --- a/PettingZoo.sln.DotSettings +++ b/PettingZoo.sln.DotSettings @@ -1,4 +1,5 @@  True + DB MQ WPF \ No newline at end of file diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index 9ce003d..b227252 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -39,9 +39,15 @@ + + + True + True + ConnectionDisplayNameStrings.resx + ConnectionWindowStrings.resx True @@ -88,6 +94,10 @@ + + PublicResXFileCodeGenerator + ConnectionDisplayNameStrings.Designer.cs + ConnectionWindowStrings.Designer.cs PublicResXFileCodeGenerator diff --git a/PettingZoo/Program.cs b/PettingZoo/Program.cs index aefc18a..d09129a 100644 --- a/PettingZoo/Program.cs +++ b/PettingZoo/Program.cs @@ -1,17 +1,14 @@ using System; using System.Globalization; -using System.IO; -using System.Reflection; using System.Windows; using System.Windows.Markup; -using Newtonsoft.Json; using PettingZoo.Core.Connection; +using PettingZoo.Core.Settings; using PettingZoo.RabbitMQ; -using PettingZoo.Settings; +using PettingZoo.Settings.LiteDB; using PettingZoo.UI.Connection; using PettingZoo.UI.Main; using PettingZoo.UI.Subscribe; -using PettingZoo.UI.Tab; using SimpleInjector; namespace PettingZoo @@ -37,11 +34,10 @@ namespace PettingZoo // See comments in RunApplication container.Options.EnableAutoVerification = false; - container.RegisterSingleton(() => new UserSettings(new AppDataSettingsSerializer("Settings.json"))); - container.Register(); container.Register(); container.Register(); + container.Register(); container.Register(); @@ -72,50 +68,9 @@ namespace PettingZoo var mainWindow = container.GetInstance(); _ = app.Run(mainWindow); } - catch (Exception) + catch (Exception e) { - // TODO Log the exception and exit - } - } - - - private class AppDataSettingsSerializer : IUserSettingsSerializer - { - private readonly string path; - private readonly string fullPath; - - - public AppDataSettingsSerializer(string filename) - { - var companyName = GetProductInfo().Company; - var productName = GetProductInfo().Product; - - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - companyName, productName); - fullPath = Path.Combine(path, filename); - } - - - public void Read(UserSettings settings) - { - if (File.Exists(fullPath)) - JsonConvert.PopulateObject(File.ReadAllText(fullPath), settings); - } - - - public void Write(UserSettings settings) - { - _ = Directory.CreateDirectory(path); - File.WriteAllText(fullPath, JsonConvert.SerializeObject(settings, Formatting.Indented)); - } - - - private T GetProductInfo() - { - var attributes = GetType().Assembly.GetCustomAttributes(typeof(T), true); - return attributes.Length == 0 - ? throw new Exception("Missing product information in assembly") - : (T) attributes[0]; + MessageBox.Show($"Fatal exception: {e.Message}", @"PettingZoo", MessageBoxButton.OK, MessageBoxImage.Error); } } } diff --git a/PettingZoo/Settings/UserSettings.cs b/PettingZoo/Settings/UserSettings.cs deleted file mode 100644 index 0d48a40..0000000 --- a/PettingZoo/Settings/UserSettings.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace PettingZoo.Settings -{ - public interface IUserSettingsSerializer - { - void Read(UserSettings settings); - void Write(UserSettings settings); - } - - - public class ConnectionWindowSettings - { - public string LastHost { get; set; } - public string LastVirtualHost { get; set; } - public int LastPort { get; set; } - public string LastUsername { get; set; } - public string LastPassword { get; set; } - - //public bool LastSubscribe { get; set; } - public string LastExchange { get; set; } - public string LastRoutingKey { get; set; } - - - public ConnectionWindowSettings() - { - LastHost = "localhost"; - LastPort = 5672; - LastVirtualHost = "/"; - LastUsername = "guest"; - LastPassword = "guest"; - - LastExchange = ""; - LastRoutingKey = "#"; - } - } - - - public class UserSettings - { - public ConnectionWindowSettings ConnectionWindow { get; } - - - private readonly IUserSettingsSerializer serializer; - - - public UserSettings(IUserSettingsSerializer serializer) - { - ConnectionWindow = new ConnectionWindowSettings(); - - this.serializer = serializer; - serializer.Read(this); - } - - - public void Save() - { - serializer.Write(this); - } - } -} diff --git a/PettingZoo/Style.xaml b/PettingZoo/Style.xaml index da8e0fe..f26734e 100644 --- a/PettingZoo/Style.xaml +++ b/PettingZoo/Style.xaml @@ -33,8 +33,12 @@ - - + + + + @@ -78,4 +82,11 @@ + + \ No newline at end of file diff --git a/PettingZoo/TODO.md b/PettingZoo/TODO.md new file mode 100644 index 0000000..c181109 --- /dev/null +++ b/PettingZoo/TODO.md @@ -0,0 +1,16 @@ +Must-have +--------- + + +Should-have +----------- +- Support undocking tabs (and redocking afterwards) +- Allow tab reordering +- Save / load publisher messages (either as templates or to disk) +- Export received messages to Tapeti JSON file / Tapeti.Cmd command-line + + +Nice-to-have +------------ +- JSON validation +- JSON syntax highlighting \ No newline at end of file diff --git a/PettingZoo/UI/BaseViewModel.cs b/PettingZoo/UI/BaseViewModel.cs index 33b7909..b6f6af2 100644 --- a/PettingZoo/UI/BaseViewModel.cs +++ b/PettingZoo/UI/BaseViewModel.cs @@ -14,14 +14,9 @@ namespace PettingZoo.UI } - protected virtual void RaiseOtherPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - protected bool SetField(ref T field, T value, IEqualityComparer? comparer = null, [CallerMemberName] string? propertyName = null, - params string[]? otherPropertiesChanged) + DelegateCommand[]? delegateCommandsChanged = null, + string[]? otherPropertiesChanged = null) { if ((comparer ?? EqualityComparer.Default).Equals(field, value)) return false; @@ -29,12 +24,18 @@ namespace PettingZoo.UI field = value; RaisePropertyChanged(propertyName); - if (otherPropertiesChanged == null) - return true; - - foreach (var otherProperty in otherPropertiesChanged) - RaisePropertyChanged(otherProperty); - + if (otherPropertiesChanged != null) + { + foreach (var otherProperty in otherPropertiesChanged) + RaisePropertyChanged(otherProperty); + } + + if (delegateCommandsChanged != null) + { + foreach (var delegateCommand in delegateCommandsChanged) + delegateCommand.RaiseCanExecuteChanged(); + } + return true; } } diff --git a/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml b/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml new file mode 100644 index 0000000..e76c9de --- /dev/null +++ b/PettingZoo/UI/Connection/ConnectionDisplayNameDialog.xaml @@ -0,0 +1,25 @@ + + + + + + -