Implemented JSON syntax checking

Implemented (re)storing of main window position
This commit is contained in:
Mark van Renswoude 2021-12-20 11:51:28 +01:00
parent 4b730b2a1a
commit 28d3548088
20 changed files with 995 additions and 38 deletions

View File

@ -0,0 +1,30 @@
using System.Threading.Tasks;
namespace PettingZoo.Core.Settings
{
public interface IUISettingsRepository
{
Task<MainWindowPositionSettings?> 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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<MainWindowPositionSettings?> 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<MainWindowPositionSettingsRecord>(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
}
}

View File

@ -1,5 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/CodeAnnotations/NamespacesWithAnnotations/=PettingZoo_002EAnnotations/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DBUI/@EntryIndexedValue">DBUI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MQ/@EntryIndexedValue">MQ</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=WPF/@EntryIndexedValue">WPF</s:String></wpf:ResourceDictionary>

View File

@ -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<IUISettingsRepository>();
var position = await uitSettingsRepository.GetMainWindowPosition();
var mainWindow = container.GetInstance<MainWindow>();
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);

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 24 24"
xml:space="preserve"
sodipodi:docname="Error.svg"
width="24"
height="24"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs130">
</defs><sodipodi:namedview
id="namedview128"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="14.344828"
inkscape:cx="-14.395433"
inkscape:cy="-2.9627404"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<g
id="g95"
transform="translate(-34,-32)">
<circle
style="fill:#ed7161"
cx="46"
cy="44"
r="12"
id="circle89" /><path
style="fill:#ffffff"
d="m 47.414,44 3.536,-3.536 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -3.536,3.536 -3.536,-3.536 c -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.391 -0.391,1.023 0,1.414 l 3.536,3.536 -3.536,3.536 c -0.391,0.391 -0.391,1.023 0,1.414 0.195,0.195 0.451,0.293 0.707,0.293 0.256,0 0.512,-0.098 0.707,-0.293 l 3.536,-3.536 3.536,3.536 c 0.195,0.195 0.451,0.293 0.707,0.293 0.256,0 0.512,-0.098 0.707,-0.293 0.391,-0.391 0.391,-1.023 0,-1.414 z"
id="path91" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

54
PettingZoo/Images/Ok.svg Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 24 24"
xml:space="preserve"
sodipodi:docname="Ok.svg"
width="24"
height="24"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs13" /><sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="7.1724138"
inkscape:cx="-11.362981"
inkscape:cy="7.1802885"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<g
id="g8"
transform="translate(-34,-32)">
<g
id="g6">
<circle
style="fill:#26b999"
cx="46"
cy="44"
r="12"
id="circle2" />
<path
style="fill:#ffffff"
d="m 52.571,38.179 c -0.455,-0.316 -1.077,-0.204 -1.392,0.25 l -5.596,8.04 -3.949,-3.242 c -0.426,-0.351 -1.057,-0.288 -1.407,0.139 -0.351,0.427 -0.289,1.057 0.139,1.407 l 4.786,3.929 c 0.18,0.147 0.404,0.227 0.634,0.227 0.045,0 0.091,-0.003 0.137,-0.009 0.276,-0.039 0.524,-0.19 0.684,-0.419 l 6.214,-8.929 c 0.315,-0.454 0.203,-1.077 -0.25,-1.393 z"
id="path4" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -5,6 +5,7 @@
<TargetFramework>net6.0-windows</TargetFramework>
<Version>0.1</Version>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<Authors>Mark van Renswoude</Authors>
<Product>Petting Zoo</Product>
<Description>Petting Zoo - a live RabbitMQ message viewer</Description>
@ -19,6 +20,8 @@
<ItemGroup>
<None Remove="Images\Dock.svg" />
<None Remove="Images\Error.svg" />
<None Remove="Images\Ok.svg" />
<None Remove="Images\PublishSend.svg" />
<None Remove="Images\Undock.svg" />
</ItemGroup>
@ -28,6 +31,8 @@
<Resource Include="Images\Connect.svg" />
<Resource Include="Images\Disconnect.svg" />
<Resource Include="Images\Dock.svg" />
<Resource Include="Images\Error.svg" />
<Resource Include="Images\Ok.svg" />
<Resource Include="Images\Publish.svg" />
<Resource Include="Images\PublishSend.svg" />
<Resource Include="Images\Subscribe.svg" />
@ -35,8 +40,9 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SharpVectors" Version="1.7.6" />
<PackageReference Include="SharpVectors" Version="1.7.7" />
<PackageReference Include="SimpleInjector" Version="5.3.2" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
@ -75,6 +81,11 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Tab\Publisher\PayloadEditorStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>PayloadEditorStrings.resx</DependentUpon>
</Compile>
<Compile Update="UI\Tab\Publisher\TapetiPublisherViewStrings.Designer.cs">
<DependentUpon>TapetiPublisherViewStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
@ -126,6 +137,10 @@
<LastGenOutput>SubscribeWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Publisher\PayloadEditorStrings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>PayloadEditorStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Publisher\TapetiPublisherViewStrings.resx">
<LastGenOutput>TapetiPublisherViewStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>

View File

@ -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<IConnectionDialog, WindowConnectionDialog>();
container.Register<ISubscribeDialog, WindowSubscribeDialog>();
container.Register<IConnectionSettingsRepository, LiteDBConnectionSettingsRepository>();
container.Register<IUISettingsRepository, LiteDBUISettingsRepository>();
container.Register<MainWindow>();
@ -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<MainWindow>();
_ = app.Run(mainWindow);
app.Run();
}
catch (Exception e)
{

View File

@ -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

View File

@ -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<T> OnPropertyChanges<T>(this DependencyObject source, DependencyProperty property)
{
return Observable.Create<T>(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));
});
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<WindowState>(WindowStateProperty)
.Subscribe(newState =>
{
WasMaximized = newState switch
{
WindowState.Maximized => true,
WindowState.Normal => false,
_ => WasMaximized
};
});
}

View File

@ -0,0 +1,31 @@
<UserControl x:Class="PettingZoo.UI.Tab.Publisher.PayloadEditorControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:publisher="clr-namespace:PettingZoo.UI.Tab.Publisher"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Background="White">
<UserControl.Resources>
<ui:EnumBooleanConverter x:Key="EnumBooleanConverter" />
</UserControl.Resources>
<DockPanel x:Name="DataContextContainer" d:DataContext="{d:DesignInstance publisher:DesignTimePayloadEditorViewModel, IsDesignTimeCreatable=True}">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Visibility="{Binding ContentTypeVisibility}" Margin="0,0,0,8">
<RadioButton Content="JSON" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Json}}" />
<RadioButton Content="Plain text" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Plain}}" />
<RadioButton Content="Other" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Other}}" />
<TextBox Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding JsonValidationVisibility}" Margin="0,8,0,0">
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationOk}" />
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationError}" />
<TextBlock Text="{Binding JsonValidationMessage}" Margin="4" />
</StackPanel>
<TextBox Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource Payload}" />
</DockPanel>
</UserControl>

View File

@ -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
{
/// <summary>
/// Interaction logic for PayloadEditorControl.xaml
/// </summary>
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<string>(ContentTypeProperty)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(value =>
{
viewModel.ContentType = value;
});
this.OnPropertyChanges<bool>(FixedJsonProperty)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(value =>
{
viewModel.FixedJson = value;
});
this.OnPropertyChanges<string>(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;
}
}
}

View File

@ -0,0 +1,108 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Tab.Publisher {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to JSON.
/// </summary>
internal static string ContentTypeJson {
get {
return ResourceManager.GetString("ContentTypeJson", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Other.
/// </summary>
internal static string ContentTypeOther {
get {
return ResourceManager.GetString("ContentTypeOther", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Plain text.
/// </summary>
internal static string ContentTypePlain {
get {
return ResourceManager.GetString("ContentTypePlain", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid JSON: {0}.
/// </summary>
internal static string JsonValidationError {
get {
return ResourceManager.GetString("JsonValidationError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Valid JSON.
/// </summary>
internal static string JsonValidationOk {
get {
return ResourceManager.GetString("JsonValidationOk", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ContentTypeJson" xml:space="preserve">
<value>JSON</value>
</data>
<data name="ContentTypeOther" xml:space="preserve">
<value>Other</value>
</data>
<data name="ContentTypePlain" xml:space="preserve">
<value>Plain text</value>
</data>
<data name="JsonValidationError" xml:space="preserve">
<value>Invalid JSON: {0}</value>
</data>
<data name="JsonValidationOk" xml:space="preserve">
<value>Valid JSON</value>
</data>
</root>

View File

@ -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<PropertyChangedEventHandler, PropertyChangedEventArgs>(
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
{
}
}

View File

@ -29,7 +29,6 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="16"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -95,38 +94,35 @@
<Label Grid.Row="2" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelProperties}" Style="{StaticResource SectionLabel}"/>
<Label Grid.Row="3" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelContentType}" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
<Label Grid.Row="3" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelCorrelationId}" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CorrelationId, UpdateSourceTrigger=PropertyChanged}" />
<Label Grid.Row="4" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelCorrelationId}" />
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding CorrelationId, UpdateSourceTrigger=PropertyChanged}" />
<Label Grid.Row="4" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelAppId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding AppId, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelAppId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding AppId, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelContentEncoding}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding ContentEncoding, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelContentEncoding}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding ContentEncoding, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelExpiration}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding Expiration, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelExpiration}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding Expiration, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelMessageId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MessageId, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="8" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelMessageId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding MessageId, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="8" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPriority}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding Priority, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="9" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPriority}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Priority, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="9" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelTimestamp}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Timestamp, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="10" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelTimestamp}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding Timestamp, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="10" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelType}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding TypeProperty, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="11" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelType}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding TypeProperty, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="12" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelUserId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="12" Grid.Column="1" Text="{Binding UserId, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Label Grid.Row="11" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelUserId}" Visibility="{Binding PropertiesExpandedVisibility}" />
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding UserId, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding PropertiesExpandedVisibility}" />
<Button Grid.Row="13" Grid.Column="1" Content="{Binding PropertiesExpandedCollapsedText}" Command="{Binding PropertiesExpandCollapseCommand}" Cursor="Hand">
<Button Grid.Row="12" Grid.Column="1" Content="{Binding PropertiesExpandedCollapsedText}" Command="{Binding PropertiesExpandCollapseCommand}" Cursor="Hand">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<ContentPresenter />
@ -134,7 +130,7 @@
</Button.Template>
</Button>
<Label Grid.Row="15" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPayload}" />
<TextBox Grid.Row="15" Grid.Column="1" Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource Payload}" Height="150" />
<Label Grid.Row="14" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPayload}" />
<publisher:PayloadEditorControl Grid.Row="14" Grid.Column="1" Payload="{Binding Payload}" ContentType="{Binding ContentType}" Height="350"/>
</ui:GridLayout>
</UserControl>

View File

@ -48,6 +48,6 @@
</Grid>
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static publisher:TapetiPublisherViewStrings.LabelPayload}" />
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource Payload}" Height="150" />
<publisher:PayloadEditorControl Grid.Row="6" Grid.Column="1" Payload="{Binding Payload}" FixedJson="True" Height="350"/>
</ui:GridLayout>
</UserControl>