From 28d35480887d12d5fa46eb79e24f566b534eed6d Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Mon, 20 Dec 2021 11:51:28 +0100 Subject: [PATCH] Implemented JSON syntax checking Implemented (re)storing of main window position --- .../Settings/IUISettingsRepository.cs | 30 ++++ .../BaseLiteDBRepository.cs | 10 +- .../LiteDBUISettingsRepository.cs | 84 ++++++++++ PettingZoo.sln.DotSettings | 2 + PettingZoo/App.xaml.cs | 75 ++++++++- PettingZoo/Images/Error.svg | 69 ++++++++ PettingZoo/Images/Ok.svg | 54 ++++++ PettingZoo/PettingZoo.csproj | 17 +- PettingZoo/Program.cs | 9 +- PettingZoo/TODO.md | 2 - PettingZoo/UI/DependencyObjectExtensions.cs | 29 ++++ PettingZoo/UI/EnumBooleanConverter.cs | 19 +++ PettingZoo/UI/Main/MainWindow.xaml.cs | 16 ++ .../Tab/Publisher/PayloadEditorControl.xaml | 31 ++++ .../Publisher/PayloadEditorControl.xaml.cs | 141 ++++++++++++++++ .../PayloadEditorStrings.Designer.cs | 108 ++++++++++++ .../Tab/Publisher/PayloadEditorStrings.resx | 135 +++++++++++++++ .../Tab/Publisher/PayloadEditorViewModel.cs | 154 ++++++++++++++++++ .../UI/Tab/Publisher/RawPublisherView.xaml | 46 +++--- .../UI/Tab/Publisher/TapetiPublisherView.xaml | 2 +- 20 files changed, 995 insertions(+), 38 deletions(-) create mode 100644 PettingZoo.Core/Settings/IUISettingsRepository.cs create mode 100644 PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs create mode 100644 PettingZoo/Images/Error.svg create mode 100644 PettingZoo/Images/Ok.svg create mode 100644 PettingZoo/UI/DependencyObjectExtensions.cs create mode 100644 PettingZoo/UI/EnumBooleanConverter.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml create mode 100644 PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs create mode 100644 PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx create mode 100644 PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs diff --git a/PettingZoo.Core/Settings/IUISettingsRepository.cs b/PettingZoo.Core/Settings/IUISettingsRepository.cs new file mode 100644 index 0000000..d32aaee --- /dev/null +++ b/PettingZoo.Core/Settings/IUISettingsRepository.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace PettingZoo.Core.Settings +{ + public interface IUISettingsRepository + { + Task GetMainWindowPosition(); + Task StoreMainWindowPosition(MainWindowPositionSettings settings); + } + + + public class MainWindowPositionSettings + { + public int Top { get; } + public int Left { get; } + public int Width { get; } + public int Height { get; } + public bool Maximized { get; } + + + public MainWindowPositionSettings(int top, int left, int width, int height, bool maximized) + { + Top = top; + Left = left; + Width = width; + Height = height; + Maximized = maximized; + } + } +} diff --git a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs index 155cd50..f255b99 100644 --- a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs +++ b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs @@ -7,6 +7,11 @@ namespace PettingZoo.Settings.LiteDB { private readonly string databaseFilename; + protected static readonly BsonMapper Mapper = new() + { + EmptyStringToNull = false + }; + public BaseLiteDBRepository(string databaseName) { @@ -24,10 +29,7 @@ namespace PettingZoo.Settings.LiteDB protected ILiteDatabaseAsync GetDatabase() { - return new LiteDatabaseAsync(databaseFilename, new BsonMapper - { - EmptyStringToNull = false - }); + return new LiteDatabaseAsync(databaseFilename, Mapper); } } } diff --git a/PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs b/PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs new file mode 100644 index 0000000..e7f54da --- /dev/null +++ b/PettingZoo.Settings.LiteDB/LiteDBUISettingsRepository.cs @@ -0,0 +1,84 @@ +using PettingZoo.Core.Settings; + +namespace PettingZoo.Settings.LiteDB +{ + public class LiteDBUISettingsRepository : BaseLiteDBRepository, IUISettingsRepository + { + private const string CollectionSettings = "settings"; + + + public LiteDBUISettingsRepository() : base(@"uiSettings") + { + } + + + public async Task GetMainWindowPosition() + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionSettings); + + var settings = await collection.FindByIdAsync(MainWindowPositionSettingsRecord.SettingsKey); + if (settings == null) + return null; + + var position = Mapper.ToObject(settings); + return new MainWindowPositionSettings( + position.Top, + position.Left, + position.Width, + position.Height, + position.Maximized); + } + + + public async Task StoreMainWindowPosition(MainWindowPositionSettings settings) + { + using var database = GetDatabase(); + var collection = database.GetCollection(CollectionSettings); + + await collection.UpsertAsync( + Mapper.ToDocument(new MainWindowPositionSettingsRecord + { + Top = settings.Top, + Left = settings.Left, + Width = settings.Width, + Height = settings.Height, + Maximized = settings.Maximized + })); + } + + + + // ReSharper disable MemberCanBePrivate.Local - for LiteDB + // ReSharper disable PropertyCanBeMadeInitOnly.Local + private class BaseSettingsRecord + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public Guid Id { get; } + + protected BaseSettingsRecord(Guid id) + { + Id = id; + } + } + + + private class MainWindowPositionSettingsRecord : BaseSettingsRecord + { + public static readonly Guid SettingsKey = new("fc71cf99-0744-4f5d-ada8-6a78d1df7b62"); + + + public int Top { get; set; } + public int Left { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public bool Maximized { get; set; } + + public MainWindowPositionSettingsRecord() : base(SettingsKey) + { + } + } + // ReSharper restore PropertyCanBeMadeInitOnly.Local + // ReSharper restore MemberCanBePrivate.Local + } +} diff --git a/PettingZoo.sln.DotSettings b/PettingZoo.sln.DotSettings index 0b00c05..e8372d6 100644 --- a/PettingZoo.sln.DotSettings +++ b/PettingZoo.sln.DotSettings @@ -1,5 +1,7 @@  True DB + DBUI + UI MQ WPF \ No newline at end of file diff --git a/PettingZoo/App.xaml.cs b/PettingZoo/App.xaml.cs index 0a82edf..40f08f2 100644 --- a/PettingZoo/App.xaml.cs +++ b/PettingZoo/App.xaml.cs @@ -1,10 +1,83 @@ -using System.Windows; +using System; +using System.Drawing; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; +using PettingZoo.Core.Settings; +using PettingZoo.UI.Main; +using SimpleInjector; +using Point = System.Windows.Point; namespace PettingZoo { public partial class App { + private readonly Container container; + + + public App() + { + throw new InvalidOperationException("Default main should not be used"); + } + + + public App(Container container) + { + this.container = container; + } + + + protected override async void OnStartup(StartupEventArgs e) + { + var uitSettingsRepository = container.GetInstance(); + var position = await uitSettingsRepository.GetMainWindowPosition(); + + var mainWindow = container.GetInstance(); + + if (position != null) + { + var positionBounds = new Rect( + new Point(position.Left, position.Top), + new Point(position.Left + position.Width, position.Top + position.Height)); + + if (InScreenBounds(positionBounds)) + { + mainWindow.WindowStartupLocation = WindowStartupLocation.Manual; + mainWindow.Top = positionBounds.Top; + mainWindow.Left = positionBounds.Left; + mainWindow.Width = positionBounds.Width; + mainWindow.Height = positionBounds.Height; + } + + mainWindow.WindowState = position.Maximized ? WindowState.Maximized : WindowState.Normal; + } + + mainWindow.Closing += (_, _) => + { + var newPosition = new MainWindowPositionSettings( + (int)mainWindow.RestoreBounds.Top, + (int)mainWindow.RestoreBounds.Left, + (int)mainWindow.RestoreBounds.Width, + (int)mainWindow.RestoreBounds.Height, + mainWindow.WasMaximized); + + Task.Run(() => uitSettingsRepository.StoreMainWindowPosition(newPosition)); + }; + + mainWindow.Show(); + } + + + private static bool InScreenBounds(Rect bounds) + { + var boundsRectangle = new Rectangle((int)bounds.Left, (int)bounds.Top, (int)bounds.Width, (int)bounds.Height); + + // There doesn't appear to be any way to get this information other than from System.Windows.From/PInvoke at the time of writing + return System.Windows.Forms.Screen.AllScreens.Any(screen => screen.Bounds.IntersectsWith(boundsRectangle)); + } + + private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { _ = MessageBox.Show($"Unhandled exception: {e.Exception.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error); diff --git a/PettingZoo/Images/Error.svg b/PettingZoo/Images/Error.svg new file mode 100644 index 0000000..968c127 --- /dev/null +++ b/PettingZoo/Images/Error.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/Images/Ok.svg b/PettingZoo/Images/Ok.svg new file mode 100644 index 0000000..fc434de --- /dev/null +++ b/PettingZoo/Images/Ok.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index 6e599ee..0ad689f 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -5,6 +5,7 @@ net6.0-windows 0.1 true + true Mark van Renswoude Petting Zoo Petting Zoo - a live RabbitMQ message viewer @@ -19,6 +20,8 @@ + + @@ -28,6 +31,8 @@ + + @@ -35,8 +40,9 @@ - + + @@ -75,6 +81,11 @@ True True + + True + True + PayloadEditorStrings.resx + TapetiPublisherViewStrings.resx True @@ -126,6 +137,10 @@ SubscribeWindowStrings.Designer.cs PublicResXFileCodeGenerator + + ResXFileCodeGenerator + PayloadEditorStrings.Designer.cs + TapetiPublisherViewStrings.Designer.cs PublicResXFileCodeGenerator diff --git a/PettingZoo/Program.cs b/PettingZoo/Program.cs index d09129a..f3e2dcf 100644 --- a/PettingZoo/Program.cs +++ b/PettingZoo/Program.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; using PettingZoo.Core.Connection; @@ -38,6 +39,7 @@ namespace PettingZoo container.Register(); container.Register(); container.Register(); + container.Register(); container.Register(); @@ -49,7 +51,7 @@ namespace PettingZoo { try { - var app = new App(); + var app = new App(container); app.InitializeComponent(); #if DEBUG @@ -64,9 +66,8 @@ namespace PettingZoo // All this is the reason we only perform verification in debug builds #endif - - var mainWindow = container.GetInstance(); - _ = app.Run(mainWindow); + + app.Run(); } catch (Exception e) { diff --git a/PettingZoo/TODO.md b/PettingZoo/TODO.md index 450c51a..9657558 100644 --- a/PettingZoo/TODO.md +++ b/PettingZoo/TODO.md @@ -1,6 +1,5 @@ Must-have --------- -- Option to not save password in profiles Should-have @@ -11,5 +10,4 @@ Should-have Nice-to-have ------------ -- JSON validation - JSON syntax highlighting \ No newline at end of file diff --git a/PettingZoo/UI/DependencyObjectExtensions.cs b/PettingZoo/UI/DependencyObjectExtensions.cs new file mode 100644 index 0000000..fea8c66 --- /dev/null +++ b/PettingZoo/UI/DependencyObjectExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows; + +namespace PettingZoo.UI +{ + public static class DependencyObjectExtensions + { + public static IObservable OnPropertyChanges(this DependencyObject source, DependencyProperty property) + { + return Observable.Create(o => + { + var dpd = DependencyPropertyDescriptor.FromProperty(property, property.OwnerType); + if (dpd == null) + o.OnError(new InvalidOperationException("Can not register change handler for this dependency property.")); + + void Handler(object? sender, EventArgs e) + { + o.OnNext((T)source.GetValue(property)); + } + + dpd?.AddValueChanged(source, Handler); + return Disposable.Create(() => dpd?.RemoveValueChanged(source, Handler)); + }); + } + } +} diff --git a/PettingZoo/UI/EnumBooleanConverter.cs b/PettingZoo/UI/EnumBooleanConverter.cs new file mode 100644 index 0000000..be439c2 --- /dev/null +++ b/PettingZoo/UI/EnumBooleanConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace PettingZoo.UI +{ + public class EnumBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value.Equals(parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return ((bool)value) ? parameter : Binding.DoNothing; + } + } +} diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs index 87737be..d017e71 100644 --- a/PettingZoo/UI/Main/MainWindow.xaml.cs +++ b/PettingZoo/UI/Main/MainWindow.xaml.cs @@ -17,6 +17,8 @@ namespace PettingZoo.UI.Main public partial class MainWindow : ITabContainer { private readonly MainWindowViewModel viewModel; + + public bool WasMaximized; public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog) @@ -28,6 +30,20 @@ namespace PettingZoo.UI.Main InitializeComponent(); Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted; + + + // If the WindowState is Minimized, we can't tell if it was maximized before. To properly store + // the last window position, keep track of it. + this.OnPropertyChanges(WindowStateProperty) + .Subscribe(newState => + { + WasMaximized = newState switch + { + WindowState.Maximized => true, + WindowState.Normal => false, + _ => WasMaximized + }; + }); } diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml new file mode 100644 index 0000000..9e45427 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs new file mode 100644 index 0000000..8fd4b99 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs @@ -0,0 +1,141 @@ +using System; +using System.Reactive.Linq; +using System.Threading; +using System.Windows; +using System.Windows.Data; + +namespace PettingZoo.UI.Tab.Publisher +{ + /// + /// Interaction logic for PayloadEditorControl.xaml + /// + public partial class PayloadEditorControl + { + private readonly PayloadEditorViewModel viewModel = new(); + + + public static readonly DependencyProperty ContentTypeProperty + = DependencyProperty.Register( + "ContentType", + typeof(string), + typeof(PayloadEditorControl), + new FrameworkPropertyMetadata("") + { + BindsTwoWayByDefault = true, + DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged + } + ); + + public string ContentType + { + get => viewModel.ContentType; + set + { + if (value == viewModel.ContentType) + return; + + SetValue(ContentTypeProperty, value); + viewModel.ContentType = value; + } + } + + + public static readonly DependencyProperty FixedJsonProperty + = DependencyProperty.Register( + "FixedJson", + typeof(bool), + typeof(PayloadEditorControl), + new PropertyMetadata(false) + ); + + public bool FixedJson + { + get => viewModel.FixedJson; + set + { + if (value == viewModel.FixedJson) + return; + + SetValue(FixedJsonProperty, value); + viewModel.FixedJson = value; + } + } + + public static readonly DependencyProperty PayloadProperty + = DependencyProperty.Register( + "Payload", + typeof(string), + typeof(PayloadEditorControl), + new FrameworkPropertyMetadata("") + { + BindsTwoWayByDefault = true, + DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged + } + ); + + public string Payload + { + get => viewModel.Payload; + set + { + if (value == viewModel.Payload) + return; + + SetValue(PayloadProperty, value); + viewModel.Payload = value; + } + } + + public PayloadEditorControl() + { + // Keep the exposed properties in sync with the ViewModel + this.OnPropertyChanges(ContentTypeProperty) + .ObserveOn(SynchronizationContext.Current!) + .Subscribe(value => + { + viewModel.ContentType = value; + }); + + + this.OnPropertyChanges(FixedJsonProperty) + .ObserveOn(SynchronizationContext.Current!) + .Subscribe(value => + { + viewModel.FixedJson = value; + }); + + + this.OnPropertyChanges(PayloadProperty) + .ObserveOn(SynchronizationContext.Current!) + .Subscribe(value => + { + viewModel.Payload = value; + }); + + + viewModel.PropertyChanged += (_, args) => + { + switch (args.PropertyName) + { + case nameof(viewModel.ContentType): + SetValue(ContentTypeProperty, viewModel.ContentType); + break; + + case nameof(viewModel.FixedJson): + SetValue(FixedJsonProperty, viewModel.FixedJson); + break; + + case nameof(viewModel.Payload): + SetValue(PayloadProperty, viewModel.Payload); + break; + } + }; + + InitializeComponent(); + + // Setting the DataContext for the UserControl is a major PITA when binding the control's properties, + // so I've moved the ViewModel one level down to get the best of both worlds... + DataContextContainer.DataContext = viewModel; + } + } +} diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs new file mode 100644 index 0000000..aff3adc --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// 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.UI.Tab.Publisher { + 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 PayloadEditorStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PayloadEditorStrings() { + } + + /// + /// 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.UI.Tab.Publisher.PayloadEditorStrings", typeof(PayloadEditorStrings).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 JSON. + /// + internal static string ContentTypeJson { + get { + return ResourceManager.GetString("ContentTypeJson", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Other. + /// + internal static string ContentTypeOther { + get { + return ResourceManager.GetString("ContentTypeOther", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plain text. + /// + internal static string ContentTypePlain { + get { + return ResourceManager.GetString("ContentTypePlain", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid JSON: {0}. + /// + internal static string JsonValidationError { + get { + return ResourceManager.GetString("JsonValidationError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Valid JSON. + /// + internal static string JsonValidationOk { + get { + return ResourceManager.GetString("JsonValidationOk", resourceCulture); + } + } + } +} diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx new file mode 100644 index 0000000..0ae56b9 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + JSON + + + Other + + + Plain text + + + Invalid JSON: {0} + + + Valid JSON + + \ No newline at end of file diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs new file mode 100644 index 0000000..1eb0149 --- /dev/null +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs @@ -0,0 +1,154 @@ +using System; +using System.ComponentModel; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Windows; +using Newtonsoft.Json.Linq; + +namespace PettingZoo.UI.Tab.Publisher +{ + public enum PayloadEditorContentType + { + Json, + Plain, + Other + }; + + + public class PayloadEditorViewModel : BaseViewModel + { + private const string ContentTypeJson = "application/json"; + private const string ContentTypePlain = "text/plain"; + + private string contentType = ContentTypeJson; + private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json; + private bool fixedJson; + + private bool jsonValid = true; + private string jsonValidationMessage; + + private string payload = ""; + + + public string ContentType + { + get => ContentTypeSelection switch + { + PayloadEditorContentType.Json => ContentTypeJson, + PayloadEditorContentType.Plain => ContentTypePlain, + _ => contentType + }; + + set + { + if (!SetField(ref contentType, value)) + return; + + ContentTypeSelection = value switch + { + ContentTypeJson => PayloadEditorContentType.Json, + ContentTypePlain => PayloadEditorContentType.Plain, + _ => PayloadEditorContentType.Other + }; + } + } + + + public PayloadEditorContentType ContentTypeSelection + { + get => contentTypeSelection; + set + { + if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(JsonValidationVisibility) })) + return; + + ContentType = ContentTypeSelection switch + { + PayloadEditorContentType.Json => ContentTypeJson, + PayloadEditorContentType.Plain => ContentTypePlain, + _ => ContentType + }; + + ValidatePayload(); + } + } + + + public bool FixedJson + { + get => fixedJson; + set => SetField(ref fixedJson, value); + } + + public Visibility JsonValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed; + public Visibility JsonValidationOk => JsonValid ? Visibility.Visible : Visibility.Collapsed; + public Visibility JsonValidationError => !JsonValid ? Visibility.Visible : Visibility.Collapsed; + + + public string JsonValidationMessage + { + get => jsonValidationMessage; + private set => SetField(ref jsonValidationMessage, value); + } + + + public bool JsonValid + { + get => jsonValid; + private set => SetField(ref jsonValid, value, otherPropertiesChanged: new[] { nameof(JsonValidationOk), nameof(JsonValidationError) }); + } + + public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible; + + + public string Payload + { + get => payload; + set => SetField(ref payload, value); + } + + + + public PayloadEditorViewModel() + { + jsonValidationMessage = PayloadEditorStrings.JsonValidationOk; + + Observable.FromEventPattern( + h => PropertyChanged += h, + h => PropertyChanged -= h) + .Where(e => e.EventArgs.PropertyName == nameof(Payload)) + .Throttle(TimeSpan.FromMilliseconds(500)) + .Subscribe(_ => ValidatePayload()); + } + + + private void ValidatePayload() + { + if (ContentTypeSelection != PayloadEditorContentType.Json) + { + JsonValid = true; + JsonValidationMessage = PayloadEditorStrings.JsonValidationOk; + return; + } + + try + { + if (!string.IsNullOrEmpty(Payload)) + JToken.Parse(Payload); + + JsonValid = true; + JsonValidationMessage = PayloadEditorStrings.JsonValidationOk; + } + catch (Exception e) + { + JsonValid = false; + JsonValidationMessage = string.Format(PayloadEditorStrings.JsonValidationError, e.Message); + } + } + } + + + public class DesignTimePayloadEditorViewModel : PayloadEditorViewModel + { + } +} diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml index 934c8d6..567ff6a 100644 --- a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml @@ -29,7 +29,6 @@ - @@ -95,38 +94,35 @@