diff --git a/PettingZoo.Core/Export/ExportFormatProvider.cs b/PettingZoo.Core/Export/ExportFormatProvider.cs new file mode 100644 index 0000000..b761511 --- /dev/null +++ b/PettingZoo.Core/Export/ExportFormatProvider.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace PettingZoo.Core.Export +{ + public class ExportFormatProvider : IExportFormatProvider + { + private readonly List formats; + + public IEnumerable Formats => formats; + + public ExportFormatProvider(params IExportFormat[] formats) + { + this.formats = new List(formats); + } + } +} diff --git a/PettingZoo.Core/Export/IExportFormat.cs b/PettingZoo.Core/Export/IExportFormat.cs new file mode 100644 index 0000000..92afbec --- /dev/null +++ b/PettingZoo.Core/Export/IExportFormat.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; + +namespace PettingZoo.Core.Export +{ + public interface IExportFormat + { + public string Filter { get; } + + public Task Export(Stream stream, IEnumerable messages); + } +} diff --git a/PettingZoo.Core/Export/IExportFormatProvider.cs b/PettingZoo.Core/Export/IExportFormatProvider.cs new file mode 100644 index 0000000..de76d7a --- /dev/null +++ b/PettingZoo.Core/Export/IExportFormatProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace PettingZoo.Core.Export +{ + public interface IExportFormatProvider + { + public IEnumerable Formats { get; } + } +} diff --git a/PettingZoo.Tapeti/Export/TapetiCmdExportFormat.cs b/PettingZoo.Tapeti/Export/TapetiCmdExportFormat.cs new file mode 100644 index 0000000..37b452d --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdExportFormat.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PettingZoo.Core.Connection; +using PettingZoo.Core.Export; + +namespace PettingZoo.Tapeti.Export +{ + public class TapetiCmdExportFormat : IExportFormat + { + public string Filter => TapetiCmdExportStrings.TapetiCmdFilter; + + + private static readonly JsonSerializerSettings SerializerSettings = new() + { + NullValueHandling = NullValueHandling.Ignore + }; + + + public async Task Export(Stream stream, IEnumerable messages) + { + await using var exportFile = new StreamWriter(stream, Encoding.UTF8); + + foreach (var message in messages) + { + var serializableMessage = new SerializableMessage + { + Exchange = message.Exchange, + RoutingKey = message.RoutingKey, + Properties = new SerializableMessageProperties + { + AppId = message.Properties.AppId, + ContentEncoding = message.Properties.ContentEncoding, + ContentType = message.Properties.ContentType, + CorrelationId = message.Properties.CorrelationId, + DeliveryMode = message.Properties.DeliveryMode switch + { + MessageDeliveryMode.Persistent => 2, + _ => 1 + }, + Expiration = message.Properties.Expiration, + Headers = message.Properties.Headers.Count > 0 ? message.Properties.Headers.ToDictionary(p => p.Key, p => p.Value) : null, + MessageId = message.Properties.MessageId, + Priority = message.Properties.Priority, + ReplyTo = message.Properties.ReplyTo, + Timestamp = message.Properties.Timestamp.HasValue ? new DateTimeOffset(message.Properties.Timestamp.Value).ToUnixTimeSeconds() : null, + Type = message.Properties.Type, + UserId = message.Properties.UserId + } + }; + + + var useRawBody = true; + if (message.Properties.ContentType == @"application/json") + { + try + { + if (JToken.Parse(Encoding.UTF8.GetString(message.Body)) is JObject jsonBody) + { + serializableMessage.Body = jsonBody; + useRawBody = false; + } + } + catch + { + // Use raw body + } + } + + if (useRawBody) + serializableMessage.RawBody = message.Body; + + var serialized = JsonConvert.SerializeObject(serializableMessage, SerializerSettings); + await exportFile.WriteLineAsync(serialized); + } + } + } + + + // It would be nicer if Tapeti.Cmd exposed it's file format in a NuGet package... if only I knew the author ¯\_(ツ)_/¯ + public class SerializableMessage + { + //public ulong DeliveryTag; + //public bool Redelivered; + public string? Exchange; + public string? RoutingKey; + //public string? Queue; + + // ReSharper disable once FieldCanBeMadeReadOnly.Local - must be settable by JSON deserialization + public SerializableMessageProperties? Properties; + + public JObject? Body; + public byte[]? RawBody; + } + + + public class SerializableMessageProperties + { + public string? AppId; + //public string? ClusterId; + public string? ContentEncoding; + public string? ContentType; + public string? CorrelationId; + public byte? DeliveryMode; + public string? Expiration; + public IDictionary? Headers; + public string? MessageId; + public byte? Priority; + public string? ReplyTo; + public long? Timestamp; + public string? Type; + public string? UserId; + } +} diff --git a/PettingZoo.Tapeti/Export/TapetiCmdExportStrings.Designer.cs b/PettingZoo.Tapeti/Export/TapetiCmdExportStrings.Designer.cs new file mode 100644 index 0000000..e6501d1 --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdExportStrings.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PettingZoo.Tapeti.Export { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TapetiCmdExportStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TapetiCmdExportStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.Export.TapetiCmdExportStrings", typeof(TapetiCmdExportStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Tapeti.Cmd single-file JSON (*.json)|*.json. + /// + internal static string TapetiCmdFilter { + get { + return ResourceManager.GetString("TapetiCmdFilter", resourceCulture); + } + } + } +} diff --git a/PettingZoo.Tapeti/Export/TapetiCmdExportStrings.resx b/PettingZoo.Tapeti/Export/TapetiCmdExportStrings.resx new file mode 100644 index 0000000..4978050 --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdExportStrings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Tapeti.Cmd single-file JSON (*.json)|*.json + + \ No newline at end of file diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj index 46db93e..1b716a5 100644 --- a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj @@ -32,6 +32,11 @@ True AssemblyParserStrings.resx + + True + True + TapetiCmdExportStrings.resx + True True @@ -57,6 +62,10 @@ ResXFileCodeGenerator AssemblyParserStrings.Designer.cs + + ResXFileCodeGenerator + TapetiCmdExportStrings.Designer.cs + PublicResXFileCodeGenerator ClassSelectionStrings.Designer.cs diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs index 038daa3..6471460 100644 --- a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs @@ -34,8 +34,6 @@ namespace PettingZoo.Tapeti .WithSourcesFrom(Path.Combine(PettingZooPaths.InstallationRoot, @"nuget.config")) .WithSourcesFrom(Path.Combine(PettingZooPaths.AppDataRoot, @"nuget.config")); - var dispatcher = Dispatcher.CurrentDispatcher; - var viewModel = new PackageSelectionViewModel(packageManager); var selectionWindow = new PackageSelectionWindow(viewModel) { @@ -44,7 +42,7 @@ namespace PettingZoo.Tapeti viewModel.Select += (_, args) => { - dispatcher.Invoke(() => + Application.Current.Dispatcher.Invoke(() => { var windowBounds = selectionWindow.RestoreBounds; selectionWindow.Close(); @@ -65,7 +63,7 @@ namespace PettingZoo.Tapeti // var classes = var examples = LoadExamples(assemblies); - dispatcher.Invoke(() => + Application.Current.Dispatcher.Invoke(() => { progressWindow.Close(); progressWindow = null; @@ -90,7 +88,7 @@ namespace PettingZoo.Tapeti } catch (Exception e) { - dispatcher.Invoke(() => + Application.Current.Dispatcher.Invoke(() => { // ReSharper disable once ConstantConditionalAccessQualifier - if I remove it, there's a "Dereference of a possibly null reference" warning instead progressWindow?.Close(); diff --git a/PettingZoo/Images/Export.svg b/PettingZoo/Images/Export.svg new file mode 100644 index 0000000..3d69fec --- /dev/null +++ b/PettingZoo/Images/Export.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Import.svg b/PettingZoo/Images/Import.svg new file mode 100644 index 0000000..9081f8f --- /dev/null +++ b/PettingZoo/Images/Import.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index 6f17964..9e71fb5 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -23,7 +23,9 @@ + + @@ -36,7 +38,9 @@ + + diff --git a/PettingZoo/Program.cs b/PettingZoo/Program.cs index 9ef7cd9..9c397b0 100644 --- a/PettingZoo/Program.cs +++ b/PettingZoo/Program.cs @@ -4,14 +4,17 @@ using System.IO; using System.Windows; using System.Windows.Markup; using PettingZoo.Core.Connection; +using PettingZoo.Core.Export; using PettingZoo.Core.Generator; using PettingZoo.Core.Settings; using PettingZoo.RabbitMQ; using PettingZoo.Settings.LiteDB; using PettingZoo.Tapeti; +using PettingZoo.Tapeti.Export; using PettingZoo.UI.Connection; using PettingZoo.UI.Main; using PettingZoo.UI.Subscribe; +using PettingZoo.UI.Tab; using Serilog; using SimpleInjector; @@ -79,6 +82,10 @@ namespace PettingZoo container.Register(); container.Register(); container.Register(); + container.Register(); + container.Register(); + + container.RegisterInstance(new ExportFormatProvider(new TapetiCmdExportFormat())); container.Register(); diff --git a/PettingZoo/TODO.md b/PettingZoo/TODO.md index 51bc24c..9e02063 100644 --- a/PettingZoo/TODO.md +++ b/PettingZoo/TODO.md @@ -5,7 +5,6 @@ Should-have ----------- - Save / load publisher messages (either as templates or to disk) -- Tapeti: export received messages to Tapeti.Cmd JSON file / Tapeti.Cmd command-line - Tapeti: import Tapeti.Cmd JSON file into Subscriber-esque tab for easier browsing - Tapeti: fetch NuGet dependencies to improve the chances of succesfully loading the assembly, instead of the current "extraAssembliesPaths" workaround diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs index 35c2dfe..74eb46e 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml.cs +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -5,7 +5,6 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using PettingZoo.Core.Connection; -using PettingZoo.Core.Generator; using PettingZoo.UI.Connection; using PettingZoo.UI.Subscribe; using PettingZoo.UI.Tab; @@ -20,11 +19,12 @@ namespace PettingZoo.UI.Main public bool WasMaximized; - public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, IExampleGenerator exampleGenerator) + public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, + ITabHostProvider tabHostProvider, ITabFactory tabFactory) { WindowStartupLocation = WindowStartupLocation.CenterScreen; - viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this, exampleGenerator) + viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this, tabHostProvider, tabFactory) { TabHostWindow = this }; diff --git a/PettingZoo/UI/Main/MainWindowViewModel.cs b/PettingZoo/UI/Main/MainWindowViewModel.cs index b8066dc..4be528f 100644 --- a/PettingZoo/UI/Main/MainWindowViewModel.cs +++ b/PettingZoo/UI/Main/MainWindowViewModel.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using PettingZoo.Core.Connection; -using PettingZoo.Core.Generator; using PettingZoo.UI.Connection; using PettingZoo.UI.Subscribe; using PettingZoo.UI.Tab; @@ -29,6 +28,7 @@ namespace PettingZoo.UI.Main private readonly IConnectionDialog connectionDialog; private readonly ISubscribeDialog subscribeDialog; private readonly ITabContainer tabContainer; + private readonly ITabHostProvider tabHostProvider; private readonly ITabFactory tabFactory; private SubscribeDialogParams? subscribeDialogParams; @@ -103,12 +103,16 @@ namespace PettingZoo.UI.Main public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, - ISubscribeDialog subscribeDialog, ITabContainer tabContainer, IExampleGenerator exampleGenerator) + ISubscribeDialog subscribeDialog, ITabContainer tabContainer, ITabHostProvider tabHostProvider, ITabFactory tabFactory) { + tabHostProvider.SetInstance(this); + this.connectionFactory = connectionFactory; this.connectionDialog = connectionDialog; this.subscribeDialog = subscribeDialog; this.tabContainer = tabContainer; + this.tabHostProvider = tabHostProvider; + this.tabFactory = tabFactory; connectionStatus = GetConnectionStatus(null); connectionStatusType = ConnectionStatusType.Error; @@ -120,8 +124,6 @@ namespace PettingZoo.UI.Main subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute); closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute); undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute); - - tabFactory = new ViewTabFactory(this, exampleGenerator); } @@ -226,7 +228,7 @@ namespace PettingZoo.UI.Main if (tab == null) return; - var tabHostWindow = UndockedTabHostWindow.Create(this, tab, tabContainer.TabWidth, tabContainer.TabHeight); + var tabHostWindow = UndockedTabHostWindow.Create(tabHostProvider, tab, tabContainer.TabWidth, tabContainer.TabHeight); undockedTabs.Add(tab, tabHostWindow); tabHostWindow.Show(); @@ -330,7 +332,7 @@ namespace PettingZoo.UI.Main public class DesignTimeMainWindowViewModel : MainWindowViewModel { - public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!, null!) + public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!, null!, null!) { } } diff --git a/PettingZoo/UI/Tab/ITabHostProvider.cs b/PettingZoo/UI/Tab/ITabHostProvider.cs new file mode 100644 index 0000000..fb833e5 --- /dev/null +++ b/PettingZoo/UI/Tab/ITabHostProvider.cs @@ -0,0 +1,9 @@ +namespace PettingZoo.UI.Tab +{ + public interface ITabHostProvider + { + public ITabHost Instance { get; } + + public void SetInstance(ITabHost instance); + } +} diff --git a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs index 04f6ba8..724963c 100644 --- a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs @@ -21,7 +21,7 @@ namespace PettingZoo.UI.Tab.Publisher private readonly IConnection connection; private readonly IExampleGenerator exampleGenerator; private readonly ITabFactory tabFactory; - private readonly ITabHost tabHost; + private readonly ITabHostProvider tabHostProvider; private bool sendToExchange = true; private string exchange = ""; @@ -156,12 +156,12 @@ namespace PettingZoo.UI.Tab.Publisher string IPublishDestination.RoutingKey => SendToExchange ? RoutingKey : Queue; - public PublisherViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null) + public PublisherViewModel(ITabHostProvider tabHostProvider, ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null) { this.connection = connection; this.exampleGenerator = exampleGenerator; this.tabFactory = tabFactory; - this.tabHost = tabHost; + this.tabHostProvider = tabHostProvider; publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute); @@ -287,7 +287,7 @@ namespace PettingZoo.UI.Tab.Publisher var subscriber = connection.Subscribe(); var tab = tabFactory.CreateSubscriberTab(connection, subscriber); - tabHost.AddTab(tab); + tabHostProvider.Instance.AddTab(tab); subscriber.Start(); return subscriber.QueueName; diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs index 9e848d8..3ae74b1 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs @@ -124,7 +124,7 @@ namespace PettingZoo.UI.Tab.Publisher { exampleGenerator.Select(tabHostWindow, example => { - Dispatcher.CurrentDispatcher.BeginInvoke(() => + Application.Current.Dispatcher.BeginInvoke(() => { switch (example) { diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs index 44adf43..92b3c2c 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewModel.cs @@ -1,26 +1,33 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; -using System.Windows.Threading; +using Microsoft.Win32; using PettingZoo.Core.Connection; +using PettingZoo.Core.Export; using PettingZoo.Core.Rendering; using PettingZoo.WPF.ViewModel; +using Serilog; +using IConnection = PettingZoo.Core.Connection.IConnection; namespace PettingZoo.UI.Tab.Subscriber { public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands, ITabActivate { - private readonly ITabHost tabHost; + private readonly ILogger logger; + private readonly ITabHostProvider tabHostProvider; private readonly ITabFactory tabFactory; private readonly IConnection connection; private readonly ISubscriber subscriber; - private readonly Dispatcher dispatcher; + private readonly IExportFormatProvider exportFormatProvider; private ReceivedMessageInfo? selectedMessage; private readonly DelegateCommand clearCommand; + private readonly DelegateCommand exportCommand; private readonly TabToolbarCommand[] toolbarCommands; private IDictionary? selectedMessageProperties; @@ -32,6 +39,7 @@ namespace PettingZoo.UI.Tab.Subscriber public ICommand ClearCommand => clearCommand; + public ICommand ExportCommand => exportCommand; // ReSharper disable once UnusedMember.Global - it is, but via a proxy public ICommand CreatePublisherCommand => createPublisherCommand; @@ -70,22 +78,25 @@ namespace PettingZoo.UI.Tab.Subscriber public IEnumerable ToolbarCommands => toolbarCommands; - public SubscriberViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, ISubscriber subscriber) + public SubscriberViewModel(ILogger logger, ITabHostProvider tabHostProvider, ITabFactory tabFactory, IConnection connection, ISubscriber subscriber, IExportFormatProvider exportFormatProvider) { - this.tabHost = tabHost; + this.logger = logger; + this.tabHostProvider = tabHostProvider; this.tabFactory = tabFactory; this.connection = connection; this.subscriber = subscriber; - - dispatcher = Dispatcher.CurrentDispatcher; + this.exportFormatProvider = exportFormatProvider; Messages = new ObservableCollectionEx(); UnreadMessages = new ObservableCollectionEx(); - clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute); + + clearCommand = new DelegateCommand(ClearExecute, HasMessagesCanExecute); + exportCommand = new DelegateCommand(ExportExecute, HasMessagesCanExecute); toolbarCommands = new[] { - new TabToolbarCommand(ClearCommand, SubscriberViewStrings.CommandClear, SvgIconHelper.LoadFromResource("/Images/Clear.svg")) + new TabToolbarCommand(ClearCommand, SubscriberViewStrings.CommandClear, SvgIconHelper.LoadFromResource("/Images/Clear.svg")), + new TabToolbarCommand(ExportCommand, SubscriberViewStrings.CommandExport, SvgIconHelper.LoadFromResource("/Images/Export.svg")) }; createPublisherCommand = new DelegateCommand(CreatePublisherExecute, CreatePublisherCanExecute); @@ -94,26 +105,82 @@ namespace PettingZoo.UI.Tab.Subscriber subscriber.Start(); } - private void ClearExecute() { Messages.Clear(); UnreadMessages.Clear(); + + HasMessagesChanged(); RaisePropertyChanged(nameof(UnreadMessagesVisibility)); - clearCommand.RaiseCanExecuteChanged(); } - private bool ClearCanExecute() + private bool HasMessagesCanExecute() { - return Messages.Count > 0; + return Messages.Count > 0 || UnreadMessages.Count > 0; + } + + + private void HasMessagesChanged() + { + clearCommand.RaiseCanExecuteChanged(); + exportCommand.RaiseCanExecuteChanged(); + } + + + private void ExportExecute() + { + var formats = exportFormatProvider.Formats.ToArray(); + + var dialog = new SaveFileDialog + { + Filter = string.Join('|', formats.Select(f => f.Filter)) + }; + + if (!dialog.ShowDialog().GetValueOrDefault()) + return; + + // 1-based? Seriously? + if (dialog.FilterIndex <= 0 || dialog.FilterIndex > formats.Length) + return; + + var messages = Messages.Concat(UnreadMessages).ToArray(); + var filename = dialog.FileName; + var format = formats[dialog.FilterIndex - 1]; + + Task.Run(async () => + { + try + { + await using var exportFile = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read); + await format.Export(exportFile, messages); + + await Application.Current.Dispatcher.BeginInvoke(() => + { + MessageBox.Show(string.Format(SubscriberViewStrings.ExportSuccess, messages.Length, filename), + SubscriberViewStrings.ExportResultTitle, + MessageBoxButton.OK, MessageBoxImage.Information); + }); + } + catch (Exception e) + { + logger.Error(e, "Error while exporting messages"); + + await Application.Current.Dispatcher.BeginInvoke(() => + { + MessageBox.Show(string.Format(SubscriberViewStrings.ExportError, e.Message), + SubscriberViewStrings.ExportResultTitle, + MessageBoxButton.OK, MessageBoxImage.Information); + }); + } + }); } private void CreatePublisherExecute() { var publisherTab = tabFactory.CreatePublisherTab(connection, SelectedMessage); - tabHost.AddTab(publisherTab); + tabHostProvider.Instance.AddTab(publisherTab); } @@ -125,7 +192,7 @@ namespace PettingZoo.UI.Tab.Subscriber private void SubscriberMessageReceived(object? sender, MessageReceivedEventArgs args) { - dispatcher.BeginInvoke(() => + Application.Current.Dispatcher.BeginInvoke(() => { if (!tabActive) { @@ -139,7 +206,7 @@ namespace PettingZoo.UI.Tab.Subscriber else Messages.Add(args.MessageInfo); - clearCommand.RaiseCanExecuteChanged(); + HasMessagesChanged(); }); } @@ -168,7 +235,7 @@ namespace PettingZoo.UI.Tab.Subscriber newMessageTimer = new Timer( _ => { - dispatcher.BeginInvoke(() => + Application.Current.Dispatcher.BeginInvoke(() => { if (UnreadMessages.Count == 0) return; @@ -210,7 +277,7 @@ namespace PettingZoo.UI.Tab.Subscriber public class DesignTimeSubscriberViewModel : SubscriberViewModel { - public DesignTimeSubscriberViewModel() : base(null!, null!, null!, new DesignTimeSubscriber()) + public DesignTimeSubscriberViewModel() : base(null!, null!, null!, null!, new DesignTimeSubscriber(), null!) { for (var i = 1; i <= 5; i++) (i > 2 ? UnreadMessages : Messages).Add(new ReceivedMessageInfo( diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs index 38af1ff..bff5aa4 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.Designer.cs @@ -69,6 +69,15 @@ namespace PettingZoo.UI.Tab.Subscriber { } } + /// + /// Looks up a localized string similar to Export.... + /// + public static string CommandExport { + get { + return ResourceManager.GetString("CommandExport", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open in new Publisher tab. /// @@ -96,6 +105,33 @@ namespace PettingZoo.UI.Tab.Subscriber { } } + /// + /// Looks up a localized string similar to Error while exporting messages: {0}. + /// + public static string ExportError { + get { + return ResourceManager.GetString("ExportError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export. + /// + public static string ExportResultTitle { + get { + return ResourceManager.GetString("ExportResultTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exported {0} message(s) to {1}. + /// + public static string ExportSuccess { + get { + return ResourceManager.GetString("ExportSuccess", resourceCulture); + } + } + /// /// Looks up a localized string similar to New messages. /// diff --git a/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx index e0d0afa..059f029 100644 --- a/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx +++ b/PettingZoo/UI/Tab/Subscriber/SubscriberViewStrings.resx @@ -120,6 +120,9 @@ Clear + + Export... + Open in new Publisher tab @@ -129,6 +132,15 @@ Persistent + + Error while exporting messages: {0} + + + Export + + + Exported {0} message(s) to {1} + New messages diff --git a/PettingZoo/UI/Tab/TabHostProvider.cs b/PettingZoo/UI/Tab/TabHostProvider.cs new file mode 100644 index 0000000..c6840fa --- /dev/null +++ b/PettingZoo/UI/Tab/TabHostProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace PettingZoo.UI.Tab +{ + public class TabHostProvider : ITabHostProvider + { + private ITabHost? instance; + + public ITabHost Instance => instance ?? throw new InvalidOperationException("ITabHost instance must be initialized before acquiring"); + + + // ReSharper disable once ParameterHidesMember + public void SetInstance(ITabHost instance) + { + this.instance = instance; + } + } +} diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs b/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs index bd46f75..750d638 100644 --- a/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs +++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostViewModel.cs @@ -10,7 +10,7 @@ namespace PettingZoo.UI.Tab.Undocked { public class UndockedTabHostViewModel : BaseViewModel, ITabActivate { - private readonly ITabHost tabHost; + private readonly ITabHostProvider tabHostProvider; private readonly ITab tab; private readonly DelegateCommand dockCommand; @@ -25,9 +25,9 @@ namespace PettingZoo.UI.Tab.Undocked public ICommand DockCommand => dockCommand; - public UndockedTabHostViewModel(ITabHost tabHost, ITab tab) + public UndockedTabHostViewModel(ITabHostProvider tabHostProvider, ITab tab) { - this.tabHost = tabHost; + this.tabHostProvider = tabHostProvider; this.tab = tab; tab.PropertyChanged += (_, args) => @@ -43,13 +43,13 @@ namespace PettingZoo.UI.Tab.Undocked private void DockCommandExecute() { - tabHost.DockTab(tab); + tabHostProvider.Instance.DockTab(tab); } public void WindowClosed() { - tabHost.UndockedTabClosed(tab); + tabHostProvider.Instance.UndockedTabClosed(tab); } diff --git a/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs index 1ea5fa5..d7c1572 100644 --- a/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs +++ b/PettingZoo/UI/Tab/Undocked/UndockedTabHostWindow.xaml.cs @@ -10,9 +10,9 @@ namespace PettingZoo.UI.Tab.Undocked /// public partial class UndockedTabHostWindow { - public static UndockedTabHostWindow Create(ITabHost tabHost, ITab tab, double width, double height) + public static UndockedTabHostWindow Create(ITabHostProvider tabHostProvider, ITab tab, double width, double height) { - var viewModel = new UndockedTabHostViewModel(tabHost, tab); + var viewModel = new UndockedTabHostViewModel(tabHostProvider, tab); var window = new UndockedTabHostWindow(viewModel) { Width = width, diff --git a/PettingZoo/UI/Tab/ViewTabFactory.cs b/PettingZoo/UI/Tab/ViewTabFactory.cs index 5731bcf..8ef5b46 100644 --- a/PettingZoo/UI/Tab/ViewTabFactory.cs +++ b/PettingZoo/UI/Tab/ViewTabFactory.cs @@ -1,26 +1,32 @@ using PettingZoo.Core.Connection; +using PettingZoo.Core.Export; using PettingZoo.Core.Generator; using PettingZoo.UI.Tab.Publisher; using PettingZoo.UI.Tab.Subscriber; +using Serilog; namespace PettingZoo.UI.Tab { public class ViewTabFactory : ITabFactory { - private readonly ITabHost tabHost; + private readonly ILogger logger; + private readonly ITabHostProvider tabHostProvider; private readonly IExampleGenerator exampleGenerator; + private readonly IExportFormatProvider exportFormatProvider; - public ViewTabFactory(ITabHost tabHost, IExampleGenerator exampleGenerator) + public ViewTabFactory(ILogger logger, ITabHostProvider tabHostProvider, IExampleGenerator exampleGenerator, IExportFormatProvider exportFormatProvider) { - this.tabHost = tabHost; + this.logger = logger; + this.tabHostProvider = tabHostProvider; this.exampleGenerator = exampleGenerator; + this.exportFormatProvider = exportFormatProvider; } public ITab CreateSubscriberTab(IConnection connection, ISubscriber subscriber) { - var viewModel = new SubscriberViewModel(tabHost, this, connection, subscriber); + var viewModel = new SubscriberViewModel(logger, tabHostProvider, this, connection, subscriber, exportFormatProvider); return new ViewTab( new SubscriberView(viewModel), viewModel, @@ -30,7 +36,7 @@ namespace PettingZoo.UI.Tab public ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null) { - var viewModel = new PublisherViewModel(tabHost, this, connection, exampleGenerator, fromReceivedMessage); + var viewModel = new PublisherViewModel(tabHostProvider, this, connection, exampleGenerator, fromReceivedMessage); return new ViewTab( new PublisherView(viewModel), viewModel,