Implemented DataAnnotation based validation for Tapeti messages
Fixed class name missing when selecting from Tapeti class Added Filter to class selection dialog Fixed or removed some minor todo's
This commit is contained in:
parent
3347ced9a6
commit
c75ea0cc62
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using PettingZoo.Core.Validation;
|
||||
|
||||
namespace PettingZoo.Core.Generator
|
||||
{
|
||||
@ -20,14 +21,11 @@ namespace PettingZoo.Core.Generator
|
||||
public string? Namespace { get; }
|
||||
public string ClassName { get; }
|
||||
|
||||
public string FullClassName => !string.IsNullOrEmpty(Namespace) ? Namespace + "." : "" + ClassName;
|
||||
public string FullClassName => (!string.IsNullOrEmpty(Namespace) ? Namespace + "." : "") + ClassName;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
public interface IValidatingExample : IExample
|
||||
public interface IValidatingExample : IExample, IPayloadValidator
|
||||
{
|
||||
bool Validate(string payload, out string validationMessage);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -18,8 +18,6 @@ namespace PettingZoo.Core.Rendering
|
||||
return contentType != null && ContentTypeHandlers.TryGetValue(contentType, out var handler)
|
||||
? handler(body)
|
||||
: Encoding.UTF8.GetString(body);
|
||||
|
||||
// ToDo hex output if required
|
||||
}
|
||||
|
||||
|
||||
|
25
PettingZoo.Core/Validation/IPayloadValidator.cs
Normal file
25
PettingZoo.Core/Validation/IPayloadValidator.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace PettingZoo.Core.Validation
|
||||
{
|
||||
public class PayloadValidationException : Exception
|
||||
{
|
||||
public TextPosition? ErrorPosition { get; }
|
||||
|
||||
|
||||
public PayloadValidationException(string message, TextPosition? errorPosition) : base(message)
|
||||
{
|
||||
ErrorPosition = errorPosition;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface IPayloadValidator
|
||||
{
|
||||
bool CanValidate();
|
||||
|
||||
/// <exception cref="PayloadValidationException" />
|
||||
void Validate(string payload);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace PettingZoo.UI
|
||||
namespace PettingZoo.Core.Validation
|
||||
{
|
||||
public readonly struct TextPosition : IEquatable<TextPosition>
|
||||
{
|
@ -1,44 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Loader;
|
||||
using Newtonsoft.Json;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.Core.Validation;
|
||||
|
||||
namespace PettingZoo.Tapeti.AssemblyParser
|
||||
{
|
||||
public class AssemblyParser : IDisposable
|
||||
{
|
||||
private readonly AssemblyLoadContext loadContext;
|
||||
private readonly string[] extraAssembliesPaths;
|
||||
private AssemblyLoadContext? loadContext;
|
||||
|
||||
|
||||
public AssemblyParser(params string[] extraAssembliesPaths)
|
||||
{
|
||||
// Using the MetadataLoadContext introduces extra complexity since types can not be compared
|
||||
// (a string from the loaded assembly does not equal our typeof(string) for example).
|
||||
// So instead we'll use a regular AssemblyLoadContext. Not ideal, and will probably cause other side-effects
|
||||
// if we're not careful, but I don't feel like writing a full metadata parser right now.
|
||||
// If you have a better idea, it's open-source! :-)
|
||||
loadContext = new AssemblyLoadContext(null, true);
|
||||
|
||||
foreach (var extraAssembly in extraAssembliesPaths.SelectMany(p => Directory.Exists(p)
|
||||
? Directory.GetFiles(p, "*.dll")
|
||||
: Enumerable.Empty<string>()))
|
||||
{
|
||||
loadContext.LoadFromAssemblyPath(extraAssembly);
|
||||
}
|
||||
this.extraAssembliesPaths = extraAssembliesPaths;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
loadContext.Unload();
|
||||
loadContext?.Unload();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<IClassTypeExample> GetExamples(Stream assemblyStream)
|
||||
{
|
||||
if (loadContext == null)
|
||||
{
|
||||
/*
|
||||
Using the MetadataLoadContext introduces extra complexity since types can not be compared
|
||||
(a string from the loaded assembly does not equal our typeof(string) for example).
|
||||
So instead we'll use a regular AssemblyLoadContext. Not ideal, and will probably cause other side-effects
|
||||
if we're not careful, but I don't feel like writing a full metadata parser right now.
|
||||
If you have a better idea, it's open-source! :-)
|
||||
*/
|
||||
loadContext = new AssemblyLoadContext(null, true);
|
||||
|
||||
foreach (var extraAssembly in extraAssembliesPaths.SelectMany(p => Directory.Exists(p)
|
||||
? Directory.GetFiles(p, "*.dll")
|
||||
: Enumerable.Empty<string>()))
|
||||
{
|
||||
loadContext.LoadFromAssemblyPath(extraAssembly);
|
||||
}
|
||||
}
|
||||
|
||||
var assembly = loadContext.LoadFromStream(assemblyStream);
|
||||
|
||||
foreach (var type in assembly.GetTypes().Where(t => t.IsClass))
|
||||
@ -47,7 +58,7 @@ namespace PettingZoo.Tapeti.AssemblyParser
|
||||
|
||||
|
||||
|
||||
private class TypeExample : IClassTypeExample
|
||||
private class TypeExample : IClassTypeExample, IValidatingExample
|
||||
{
|
||||
public string AssemblyName => type.Assembly.GetName().Name ?? "";
|
||||
public string? Namespace => type.Namespace;
|
||||
@ -55,6 +66,9 @@ namespace PettingZoo.Tapeti.AssemblyParser
|
||||
|
||||
private readonly Type type;
|
||||
|
||||
private bool validationInitialized;
|
||||
private bool validationAvailable;
|
||||
|
||||
|
||||
public TypeExample(Type type)
|
||||
{
|
||||
@ -64,20 +78,85 @@ namespace PettingZoo.Tapeti.AssemblyParser
|
||||
|
||||
public string Generate()
|
||||
{
|
||||
/*
|
||||
We can't create an instance of the type to serialize easily, as most will depend on
|
||||
assemblies not included in the NuGet package, so we'll parse the Type ourselves.
|
||||
This is still much easier than using MetadataReader, as we can more easily check against
|
||||
standard types like Nullable.
|
||||
|
||||
The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models
|
||||
themselves should not inherit from classes outside of their assembly, or include properties
|
||||
with types from other assemblies. With that assumption, walking the class structure should be safe.
|
||||
The extraAssemblies passed to TapetiClassLibraryExampleSource can also be used to give it a better chance.
|
||||
*/
|
||||
var serialized = TypeToJObjectConverter.Convert(type);
|
||||
return serialized.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
|
||||
public bool CanValidate()
|
||||
{
|
||||
return InitializeValidation();
|
||||
}
|
||||
|
||||
|
||||
public void Validate(string payload)
|
||||
{
|
||||
if (!InitializeValidation())
|
||||
return;
|
||||
|
||||
// Json exceptions are already handled by the PayloadEditorViewModel
|
||||
var deserialized = JsonConvert.DeserializeObject(payload, type);
|
||||
if (deserialized == null)
|
||||
throw new PayloadValidationException(AssemblyParserStrings.JsonDeserializationNull, null);
|
||||
|
||||
try
|
||||
{
|
||||
var validationContext = new ValidationContext(deserialized);
|
||||
Validator.ValidateObject(deserialized, validationContext, true);
|
||||
}
|
||||
catch (ValidationException e)
|
||||
{
|
||||
var members = string.Join(", ", e.ValidationResult.MemberNames);
|
||||
if (!string.IsNullOrEmpty(members))
|
||||
throw new PayloadValidationException(string.Format(AssemblyParserStrings.ValidationErrorsMembers, members, e.ValidationResult.ErrorMessage), null);
|
||||
|
||||
throw new PayloadValidationException(string.Format(AssemblyParserStrings.ValidationErrors, e.ValidationResult.ErrorMessage), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool InitializeValidation()
|
||||
{
|
||||
if (validationInitialized)
|
||||
return validationAvailable;
|
||||
|
||||
// Attempt to create an instance (only works if all dependencies are present, which is not yet
|
||||
// guaranteed because we aren't fetching NuGet dependencies yet). We're giving it a fighting chance
|
||||
// by referencing Tapeti.Annotations, System.ComponentModel.Annotations and Tapeti.DataAnnotations.Extensions in
|
||||
// this class library.
|
||||
try
|
||||
{
|
||||
var instance = Activator.CreateInstance(type);
|
||||
if (instance != null)
|
||||
{
|
||||
// Attributes are only evaluated when requested, so call validation once to give it a better chance to
|
||||
// detect if we'll be able to validate the message
|
||||
try
|
||||
{
|
||||
var validationContext = new ValidationContext(instance);
|
||||
Validator.ValidateObject(instance, validationContext, true);
|
||||
|
||||
validationAvailable = true;
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
// The fact that it validated is good enough, this can be expected with an empty object
|
||||
validationAvailable = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No go, try to create an example without validation
|
||||
}
|
||||
|
||||
validationInitialized = true;
|
||||
return validationAvailable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
90
PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.Designer.cs
generated
Normal file
90
PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.Designer.cs
generated
Normal file
@ -0,0 +1,90 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <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.Tapeti.AssemblyParser {
|
||||
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 AssemblyParserStrings {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal AssemblyParserStrings() {
|
||||
}
|
||||
|
||||
/// <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.Tapeti.AssemblyParser.AssemblyParserStrings", typeof(AssemblyParserStrings).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 deserialization returned null.
|
||||
/// </summary>
|
||||
internal static string JsonDeserializationNull {
|
||||
get {
|
||||
return ResourceManager.GetString("JsonDeserializationNull", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed validation: {0}.
|
||||
/// </summary>
|
||||
internal static string ValidationErrors {
|
||||
get {
|
||||
return ResourceManager.GetString("ValidationErrors", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed validation for {0}: {1}.
|
||||
/// </summary>
|
||||
internal static string ValidationErrorsMembers {
|
||||
get {
|
||||
return ResourceManager.GetString("ValidationErrorsMembers", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.resx
Normal file
129
PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.resx
Normal file
@ -0,0 +1,129 @@
|
||||
<?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=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="JsonDeserializationNull" xml:space="preserve">
|
||||
<value>JSON deserialization returned null</value>
|
||||
</data>
|
||||
<data name="ValidationErrors" xml:space="preserve">
|
||||
<value>Failed validation: {0}</value>
|
||||
</data>
|
||||
<data name="ValidationErrorsMembers" xml:space="preserve">
|
||||
<value>Failed validation for {0}: {1}</value>
|
||||
</data>
|
||||
</root>
|
@ -27,6 +27,7 @@ namespace PettingZoo.Tapeti.NuGet
|
||||
public NuGetPackageManager(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
|
||||
cache = new SourceCacheContext();
|
||||
sources = new List<Source>
|
||||
{
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NuGet.PackageManagement" Version="6.0.0" />
|
||||
<PackageReference Include="NuGet.Packaging" Version="6.0.0" />
|
||||
<PackageReference Include="NuGet.Protocol" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
@ -16,6 +17,7 @@
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="6.0.0" />
|
||||
<PackageReference Include="Tapeti.Annotations" Version="3.0.0" />
|
||||
<PackageReference Include="Tapeti.DataAnnotations.Extensions" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -25,6 +27,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="AssemblyParser\AssemblyParserStrings.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>AssemblyParserStrings.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\ClassSelection\ClassSelectionStrings.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
@ -46,6 +53,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="AssemblyParser\AssemblyParserStrings.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AssemblyParserStrings.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="UI\ClassSelection\ClassSelectionStrings.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>ClassSelectionStrings.Designer.cs</LastGenOutput>
|
||||
|
@ -51,7 +51,7 @@ namespace PettingZoo.Tapeti
|
||||
|
||||
var progressWindow = new PackageProgressWindow();
|
||||
progressWindow.Left = windowBounds.Left + (windowBounds.Width - progressWindow.Width) / 2;
|
||||
progressWindow.Left = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2;
|
||||
progressWindow.Top = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2;
|
||||
progressWindow.Show();
|
||||
|
||||
Task.Run(async () =>
|
||||
|
@ -43,9 +43,6 @@ namespace PettingZoo.Tapeti
|
||||
|
||||
foreach (var propertyInfo in classType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
// Note: unfortunately we can not call GetCustomAttributes here for now, as that would
|
||||
// trigger assemblies not included in the package to be loaded, which may not exist
|
||||
|
||||
var value = TypeToJToken(propertyInfo.PropertyType, newTypesEncountered);
|
||||
result.Add(propertyInfo.Name, value);
|
||||
}
|
||||
@ -62,6 +59,8 @@ namespace PettingZoo.Tapeti
|
||||
actualType = equivalentType;
|
||||
|
||||
|
||||
// TODO check for JsonConverter attribute? doubt we'll be able to generate a nice value for it, but at least we can provide a placeholder
|
||||
|
||||
// String is also a class
|
||||
if (actualType == typeof(string))
|
||||
return "";
|
||||
|
@ -78,6 +78,24 @@ namespace PettingZoo.Tapeti.UI.ClassSelection {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Only classes ending in "Message".
|
||||
/// </summary>
|
||||
public static string CheckboxMessageOnly {
|
||||
get {
|
||||
return ResourceManager.GetString("CheckboxMessageOnly", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Filter.
|
||||
/// </summary>
|
||||
public static string LabelFilter {
|
||||
get {
|
||||
return ResourceManager.GetString("LabelFilter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select class.
|
||||
/// </summary>
|
||||
|
@ -112,10 +112,10 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.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>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ButtonCancel" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
@ -123,6 +123,12 @@
|
||||
<data name="ButtonSelect" xml:space="preserve">
|
||||
<value>Select</value>
|
||||
</data>
|
||||
<data name="CheckboxMessageOnly" xml:space="preserve">
|
||||
<value>Only classes ending in "Message"</value>
|
||||
</data>
|
||||
<data name="LabelFilter" xml:space="preserve">
|
||||
<value>Filter</value>
|
||||
</data>
|
||||
<data name="WindowTitle" xml:space="preserve">
|
||||
<value>Select class</value>
|
||||
</data>
|
||||
|
@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Input;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
@ -13,8 +17,34 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
private BaseClassTreeItem? selectedItem;
|
||||
private readonly DelegateCommand selectCommand;
|
||||
|
||||
private string filterText = "";
|
||||
private bool filterMessageOnly = true;
|
||||
private ObservableCollection<BaseClassTreeItem> filteredExamples;
|
||||
|
||||
public ObservableCollection<BaseClassTreeItem> Examples { get; } = new();
|
||||
|
||||
public string FilterText
|
||||
{
|
||||
get => filterText;
|
||||
set => SetField(ref filterText, value);
|
||||
}
|
||||
|
||||
|
||||
public bool FilterMessageOnly
|
||||
{
|
||||
get => filterMessageOnly;
|
||||
set => SetField(ref filterMessageOnly, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<BaseClassTreeItem> FilteredExamples
|
||||
{
|
||||
get => filteredExamples;
|
||||
|
||||
[MemberNotNull(nameof(filteredExamples))]
|
||||
set => SetField(ref filteredExamples!, value);
|
||||
}
|
||||
|
||||
|
||||
public BaseClassTreeItem? SelectedItem
|
||||
{
|
||||
get => selectedItem;
|
||||
@ -39,6 +69,29 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
selectCommand = new DelegateCommand(SelectExecute, SelectCanExecute);
|
||||
|
||||
TreeFromExamples(examples);
|
||||
|
||||
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
|
||||
h => PropertyChanged += h,
|
||||
h => PropertyChanged -= h)
|
||||
.Where(e => e.EventArgs.PropertyName is nameof(FilterText) or nameof(FilterMessageOnly))
|
||||
.Throttle(TimeSpan.FromMilliseconds(250))
|
||||
.ObserveOn(SynchronizationContext.Current!)
|
||||
.Subscribe(_ => UpdateFilteredExamples());
|
||||
|
||||
UpdateFilteredExamples();
|
||||
}
|
||||
|
||||
|
||||
[MemberNotNull(nameof(filteredExamples))]
|
||||
private void UpdateFilteredExamples()
|
||||
{
|
||||
if (!FilterMessageOnly && string.IsNullOrWhiteSpace(FilterText))
|
||||
FilteredExamples = Examples;
|
||||
|
||||
FilteredExamples = new ObservableCollection<BaseClassTreeItem>(Examples
|
||||
.Select(i => i.Filter(FilterText, FilterMessageOnly))
|
||||
.Where(i => i != null)
|
||||
.Cast<BaseClassTreeItem>());
|
||||
}
|
||||
|
||||
|
||||
@ -109,7 +162,7 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
}
|
||||
|
||||
|
||||
public class BaseClassTreeItem
|
||||
public abstract class BaseClassTreeItem
|
||||
{
|
||||
private readonly SortedSet<BaseClassTreeItem> children = new(new BaseClassTreeItemComparer());
|
||||
|
||||
@ -117,7 +170,7 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
public IReadOnlyCollection<BaseClassTreeItem> Children => children;
|
||||
|
||||
|
||||
public BaseClassTreeItem(string name)
|
||||
protected BaseClassTreeItem(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
@ -127,6 +180,9 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
{
|
||||
children.Add(item);
|
||||
}
|
||||
|
||||
|
||||
public abstract BaseClassTreeItem? Filter(string filterText, bool messageOnly);
|
||||
}
|
||||
|
||||
|
||||
@ -142,6 +198,32 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
Name = string.IsNullOrEmpty(parentFolderName) ? Name : parentFolderName + "." + Name;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public override BaseClassTreeItem? Filter(string filterText, bool messageOnly)
|
||||
{
|
||||
var childFilterText = filterText;
|
||||
|
||||
// If the folder name matches, include everything in it (...that matches messageOnly)
|
||||
if (!string.IsNullOrWhiteSpace(filterText) && Name.Contains(filterText, StringComparison.CurrentCultureIgnoreCase))
|
||||
childFilterText = "";
|
||||
|
||||
var filteredChildren = Children
|
||||
.Select(c => c.Filter(childFilterText, messageOnly))
|
||||
.Where(c => c != null)
|
||||
.Cast<BaseClassTreeItem>();
|
||||
|
||||
var result = new NamespaceFolderClassTreeItem(Name);
|
||||
var hasChildren = false;
|
||||
|
||||
foreach (var filteredChild in filteredChildren)
|
||||
{
|
||||
result.AddChild(filteredChild);
|
||||
hasChildren = true;
|
||||
}
|
||||
|
||||
return hasChildren ? result : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -154,6 +236,18 @@ namespace PettingZoo.Tapeti.UI.ClassSelection
|
||||
{
|
||||
Example = example;
|
||||
}
|
||||
|
||||
|
||||
public override BaseClassTreeItem? Filter(string filterText, bool messageOnly)
|
||||
{
|
||||
// Assumes examples don't have child items, so no further filtering is required
|
||||
if (messageOnly && !Name.EndsWith(@"Message", StringComparison.CurrentCultureIgnoreCase))
|
||||
return null;
|
||||
|
||||
return string.IsNullOrWhiteSpace(filterText) || Name.Contains(filterText, StringComparison.CurrentCultureIgnoreCase)
|
||||
? this
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,7 +12,8 @@
|
||||
Width="800"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
d:DataContext="{d:DesignInstance classSelection:DesignTimeClassSelectionViewModel, IsDesignTimeCreatable=True}">
|
||||
d:DataContext="{d:DesignInstance classSelection:DesignTimeClassSelectionViewModel, IsDesignTimeCreatable=True}"
|
||||
FocusManager.FocusedElement="{Binding ElementName=DefaultFocus}">
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="TreeItemIcon" TargetType="{x:Type Image}">
|
||||
@ -31,11 +32,24 @@
|
||||
|
||||
<controls:GridLayout Style="{StaticResource Form}" Margin="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TreeView Grid.Row="0" ItemsSource="{Binding Examples}" SelectedItemChanged="TreeView_OnSelectedItemChanged">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{x:Static classSelection:ClassSelectionStrings.LabelFilter}" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}" x:Name="DefaultFocus" />
|
||||
|
||||
<CheckBox Grid.Row="1" Grid.Column="1" Content="{x:Static classSelection:ClassSelectionStrings.CheckboxMessageOnly}" IsChecked="{Binding FilterMessageOnly}" />
|
||||
|
||||
<TreeView Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding FilteredExamples}" SelectedItemChanged="TreeView_OnSelectedItemChanged">
|
||||
<TreeView.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type TreeViewItem}">
|
||||
<Setter Property="IsExpanded" Value="True" />
|
||||
@ -54,6 +68,11 @@
|
||||
</HierarchicalDataTemplate>
|
||||
<HierarchicalDataTemplate DataType="{x:Type classSelection:ExampleTreeItem}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel.InputBindings>
|
||||
<MouseBinding
|
||||
MouseAction="LeftDoubleClick"
|
||||
Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}" />
|
||||
</StackPanel.InputBindings>
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Example.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource TreeItemIcon}"/>
|
||||
<TextBlock Text="{Binding Name}" Style="{StaticResource TreeItemLabel}" />
|
||||
</StackPanel>
|
||||
@ -61,9 +80,9 @@
|
||||
</TreeView.Resources>
|
||||
</TreeView>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource FooterPanel}">
|
||||
<StackPanel Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource FooterPanel}">
|
||||
<Button Content="{x:Static classSelection:ClassSelectionStrings.ButtonSelect}" Style="{StaticResource FooterButton}" Command="{Binding SelectCommand}" />
|
||||
<Button Content="{x:Static classSelection:ClassSelectionStrings.ButtonCancel}" Style="{StaticResource FooterButton}" />
|
||||
<Button IsCancel="True" Content="{x:Static classSelection:ClassSelectionStrings.ButtonCancel}" Style="{StaticResource FooterButton}" />
|
||||
</StackPanel>
|
||||
</controls:GridLayout>
|
||||
</Window>
|
||||
|
@ -8,10 +8,11 @@
|
||||
mc:Ignorable="d"
|
||||
Title="{x:Static packageSelection:PackageSelectionStrings.WindowTitle}"
|
||||
Height="600"
|
||||
Width="800"
|
||||
Width="600"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
d:DataContext="{d:DesignInstance packageSelection:DesignTimePackageSelectionViewModel, IsDesignTimeCreatable=True}">
|
||||
d:DataContext="{d:DesignInstance packageSelection:DesignTimePackageSelectionViewModel, IsDesignTimeCreatable=True}"
|
||||
FocusManager.FocusedElement="{Binding ElementName=DefaultFocus}">
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="PackageTitle" TargetType="{x:Type TextBlock}">
|
||||
@ -28,6 +29,10 @@
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Hint" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="{x:Static SystemColors.GrayTextBrush}" />
|
||||
</Style>
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/PettingZoo.WPF;component/Style.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
@ -68,7 +73,7 @@
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding NuGetSearchTerm, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding NuGetSearchTerm, UpdateSourceTrigger=PropertyChanged}" x:Name="DefaultFocus" />
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Text="{x:Static packageSelection:PackageSelectionStrings.PlaceholderNuGetSearch}" Visibility="{Binding NuGetSearchTermPlaceholderVisibility}" Style="{StaticResource Placeholder}" />
|
||||
|
||||
<CheckBox Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="2" Content="{x:Static packageSelection:PackageSelectionStrings.CheckPrerelease}" IsChecked="{Binding NuGetIncludePrerelease}" />
|
||||
@ -82,6 +87,11 @@
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.InputBindings>
|
||||
<MouseBinding
|
||||
MouseAction="LeftDoubleClick"
|
||||
Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}" />
|
||||
</Grid.InputBindings>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@ -116,11 +126,11 @@
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding HintNuGetSources}" TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding HintNuGetSources}" TextWrapping="Wrap" Style="{StaticResource Hint}" />
|
||||
|
||||
<StackPanel Grid.Row="11" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource FooterPanel}">
|
||||
<Button Content="{x:Static packageSelection:PackageSelectionStrings.ButtonSelect}" Style="{StaticResource FooterButton}" Command="{Binding SelectCommand}" />
|
||||
<Button Content="{x:Static packageSelection:PackageSelectionStrings.ButtonCancel}" Style="{StaticResource FooterButton}" />
|
||||
<Button IsCancel="True" Content="{x:Static packageSelection:PackageSelectionStrings.ButtonCancel}" Style="{StaticResource FooterButton}" />
|
||||
</StackPanel>
|
||||
</controls:GridLayout>
|
||||
</Window>
|
||||
|
@ -12,4 +12,3 @@ Should-have
|
||||
|
||||
Nice-to-have
|
||||
------------
|
||||
- Validation against message classes (for Tapeti messages)
|
@ -2,6 +2,7 @@
|
||||
using System.Windows.Media;
|
||||
using ICSharpCode.AvalonEdit.Document;
|
||||
using ICSharpCode.AvalonEdit.Rendering;
|
||||
using PettingZoo.Core.Validation;
|
||||
|
||||
namespace PettingZoo.UI
|
||||
{
|
||||
|
@ -2,8 +2,6 @@
|
||||
using System.Windows.Input;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
|
||||
// TODO validate input
|
||||
|
||||
namespace PettingZoo.UI.Subscribe
|
||||
{
|
||||
public class SubscribeViewModel : BaseViewModel
|
||||
@ -11,28 +9,30 @@ namespace PettingZoo.UI.Subscribe
|
||||
private string exchange;
|
||||
private string routingKey;
|
||||
|
||||
private readonly DelegateCommand okCommand;
|
||||
|
||||
|
||||
public string Exchange
|
||||
{
|
||||
get => exchange;
|
||||
set => SetField(ref exchange, value);
|
||||
set => SetField(ref exchange, value, delegateCommandsChanged: new [] { okCommand });
|
||||
}
|
||||
|
||||
public string RoutingKey
|
||||
{
|
||||
get => routingKey;
|
||||
set => SetField(ref routingKey, value);
|
||||
set => SetField(ref routingKey, value, delegateCommandsChanged: new[] { okCommand });
|
||||
}
|
||||
|
||||
|
||||
public ICommand OkCommand { get; }
|
||||
public ICommand OkCommand => okCommand;
|
||||
|
||||
public event EventHandler? OkClick;
|
||||
|
||||
|
||||
public SubscribeViewModel(SubscribeDialogParams subscribeParams)
|
||||
{
|
||||
OkCommand = new DelegateCommand(OkExecute, OkCanExecute);
|
||||
okCommand = new DelegateCommand(OkExecute, OkCanExecute);
|
||||
|
||||
exchange = subscribeParams.Exchange;
|
||||
routingKey = subscribeParams.RoutingKey;
|
||||
@ -51,9 +51,9 @@ namespace PettingZoo.UI.Subscribe
|
||||
}
|
||||
|
||||
|
||||
private static bool OkCanExecute()
|
||||
private bool OkCanExecute()
|
||||
{
|
||||
return true;
|
||||
return !string.IsNullOrWhiteSpace(Exchange) && !string.IsNullOrWhiteSpace(RoutingKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,10 +30,10 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Label Grid.Column="0" Grid.Row="0" Content="{x:Static subscribe:SubscribeWindowStrings.LabelExchange}"/>
|
||||
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Exchange}" Name="ExchangeTextBox"/>
|
||||
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Exchange, UpdateSourceTrigger=PropertyChanged}" Name="ExchangeTextBox"/>
|
||||
|
||||
<Label Grid.Column="0" Grid.Row="1" Content="{x:Static subscribe:SubscribeWindowStrings.LabelRoutingKey}"/>
|
||||
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding RoutingKey}"/>
|
||||
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding RoutingKey, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</controls:GridLayout>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
|
@ -19,7 +19,7 @@ namespace PettingZoo.UI.Subscribe {
|
||||
// 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", "16.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class SubscribeWindowStrings {
|
||||
|
@ -112,10 +112,10 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.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>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ButtonCancel" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
|
@ -3,6 +3,7 @@ using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using PettingZoo.Core.Validation;
|
||||
|
||||
namespace PettingZoo.UI.Tab.Publisher
|
||||
{
|
||||
@ -87,6 +88,13 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
}
|
||||
|
||||
|
||||
public IPayloadValidator? Validator
|
||||
{
|
||||
get => viewModel.Validator;
|
||||
set => viewModel.Validator = value;
|
||||
}
|
||||
|
||||
|
||||
private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new();
|
||||
|
||||
public PayloadEditorControl()
|
||||
@ -181,7 +189,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
|
||||
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
|
||||
|
||||
// TODO this can probably be optimized to only redraw the affected line
|
||||
// This can probably be optimized to only redraw the affected line, but the message is typically so small it's not worth the effort at the moment
|
||||
Editor.TextArea.TextView.Redraw();
|
||||
break;
|
||||
|
||||
|
@ -105,6 +105,15 @@ namespace PettingZoo.UI.Tab.Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Valid syntax.
|
||||
/// </summary>
|
||||
internal static string ValidationOkSyntax {
|
||||
get {
|
||||
return ResourceManager.GetString("ValidationOkSyntax", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Validating....
|
||||
/// </summary>
|
||||
|
@ -132,6 +132,9 @@
|
||||
<data name="ValidationOk" xml:space="preserve">
|
||||
<value>Valid</value>
|
||||
</data>
|
||||
<data name="ValidationOkSyntax" xml:space="preserve">
|
||||
<value>Valid syntax</value>
|
||||
</data>
|
||||
<data name="ValidationValidating" xml:space="preserve">
|
||||
<value>Validating...</value>
|
||||
</data>
|
||||
|
@ -5,6 +5,7 @@ using System.Windows;
|
||||
using ICSharpCode.AvalonEdit.Highlighting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PettingZoo.Core.Validation;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
|
||||
namespace PettingZoo.UI.Tab.Publisher
|
||||
@ -22,6 +23,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
NotSupported,
|
||||
Validating,
|
||||
Ok,
|
||||
OkSyntax,
|
||||
Error
|
||||
}
|
||||
|
||||
@ -41,6 +43,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
ValidationStatus.NotSupported => "",
|
||||
ValidationStatus.Validating => PayloadEditorStrings.ValidationValidating,
|
||||
ValidationStatus.Ok => PayloadEditorStrings.ValidationOk,
|
||||
ValidationStatus.OkSyntax => PayloadEditorStrings.ValidationOkSyntax,
|
||||
ValidationStatus.Error => throw new InvalidOperationException(@"Message required for Error validation status"),
|
||||
_ => throw new ArgumentException(@"Unsupported validation status", nameof(status))
|
||||
};
|
||||
@ -58,7 +61,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json;
|
||||
private bool fixedJson;
|
||||
|
||||
private ValidationInfo validationInfo = new(ValidationStatus.Ok);
|
||||
private ValidationInfo validationInfo = new(ValidationStatus.OkSyntax);
|
||||
|
||||
private string payload = "";
|
||||
|
||||
@ -125,7 +128,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
|
||||
public string ValidationMessage => ValidationInfo.Message;
|
||||
|
||||
public Visibility ValidationOk => ValidationInfo.Status == ValidationStatus.Ok ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility ValidationOk => ValidationInfo.Status is ValidationStatus.Ok or ValidationStatus.OkSyntax ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility ValidationError => ValidationInfo.Status == ValidationStatus.Error ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility ValidationValidating => ValidationInfo.Status == ValidationStatus.Validating ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
@ -145,6 +148,8 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
: null;
|
||||
|
||||
|
||||
public IPayloadValidator? Validator { get; set; }
|
||||
|
||||
|
||||
public PayloadEditorViewModel()
|
||||
{
|
||||
@ -188,9 +193,28 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Payload))
|
||||
JToken.Parse(Payload);
|
||||
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Ok);
|
||||
{
|
||||
if (Validator != null && Validator.CanValidate())
|
||||
{
|
||||
Validator.Validate(payload);
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
JToken.Parse(Payload);
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.OkSyntax);
|
||||
}
|
||||
}
|
||||
else
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.OkSyntax);
|
||||
}
|
||||
catch (PayloadValidationException e)
|
||||
{
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message, e.ErrorPosition);
|
||||
}
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message, new TextPosition(e.LineNumber, e.LinePosition));
|
||||
}
|
||||
catch (JsonReaderException e)
|
||||
{
|
||||
|
@ -217,12 +217,34 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
|
||||
// TODO check parsing of priority and timestamp
|
||||
byte? priorityValue = null;
|
||||
DateTime? timestampValue = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Priority))
|
||||
{
|
||||
if (byte.TryParse(Priority, out var priorityParsedValue))
|
||||
priorityValue = priorityParsedValue;
|
||||
else
|
||||
{
|
||||
MessageBox.Show(RawPublisherViewStrings.PriorityParseFailed, RawPublisherViewStrings.PublishValidationErrorTitle, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Timestamp))
|
||||
{
|
||||
if (DateTime.TryParse(Timestamp, out var timestampParsedValue))
|
||||
timestampValue = timestampParsedValue;
|
||||
else
|
||||
{
|
||||
MessageBox.Show(RawPublisherViewStrings.TimestampParseFailed, RawPublisherViewStrings.PublishValidationErrorTitle, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var headers = Headers.Where(h => h.IsValid()).ToDictionary(h => h.Key, h => h.Value);
|
||||
|
||||
// TODO background worker / async
|
||||
|
||||
connection.Publish(new PublishMessageInfo(
|
||||
publishDestination.Exchange,
|
||||
publishDestination.RoutingKey,
|
||||
@ -236,9 +258,9 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
DeliveryMode = deliveryMode,
|
||||
Expiration = NullIfEmpty(Expiration),
|
||||
MessageId = NullIfEmpty(MessageId),
|
||||
Priority = !string.IsNullOrEmpty(Priority) && byte.TryParse(Priority, out var priorityValue) ? priorityValue : null,
|
||||
Priority = priorityValue,
|
||||
ReplyTo = publishDestination.GetReplyTo(),
|
||||
Timestamp = !string.IsNullOrEmpty(Timestamp) && DateTime.TryParse(Timestamp, out var timestampValue) ? timestampValue : null,
|
||||
Timestamp = timestampValue,
|
||||
Type = NullIfEmpty(TypeProperty),
|
||||
UserId = NullIfEmpty(UserId)
|
||||
}));
|
||||
|
@ -222,6 +222,15 @@ namespace PettingZoo.UI.Tab.Publisher {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Priority must be a valid byte value.
|
||||
/// </summary>
|
||||
public static string PriorityParseFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("PriorityParseFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to ⏶ Collapse.
|
||||
/// </summary>
|
||||
@ -239,5 +248,23 @@ namespace PettingZoo.UI.Tab.Publisher {
|
||||
return ResourceManager.GetString("PropertiesExpand", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Invalid parameters.
|
||||
/// </summary>
|
||||
public static string PublishValidationErrorTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("PublishValidationErrorTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Timestamp must be a valid date/time value.
|
||||
/// </summary>
|
||||
public static string TimestampParseFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("TimestampParseFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,10 +112,10 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.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>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="DeliveryModeNonPersistent" xml:space="preserve">
|
||||
<value>Transient (non-persistent)</value>
|
||||
@ -171,10 +171,19 @@
|
||||
<data name="LabelUserId" xml:space="preserve">
|
||||
<value>User ID</value>
|
||||
</data>
|
||||
<data name="PriorityParseFailed" xml:space="preserve">
|
||||
<value>Priority must be a valid byte value</value>
|
||||
</data>
|
||||
<data name="PropertiesCollapse" xml:space="preserve">
|
||||
<value>⏶ Collapse</value>
|
||||
</data>
|
||||
<data name="PropertiesExpand" xml:space="preserve">
|
||||
<value>⏷ Expand</value>
|
||||
</data>
|
||||
<data name="PublishValidationErrorTitle" xml:space="preserve">
|
||||
<value>Invalid parameters</value>
|
||||
</data>
|
||||
<data name="TimestampParseFailed" xml:space="preserve">
|
||||
<value>Timestamp must be a valid date/time value</value>
|
||||
</data>
|
||||
</root>
|
@ -46,6 +46,6 @@
|
||||
</Grid>
|
||||
|
||||
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static publisher:TapetiPublisherViewStrings.LabelPayload}" />
|
||||
<publisher:PayloadEditorControl Grid.Row="6" Grid.Column="1" Payload="{Binding Payload}" FixedJson="True" Height="350"/>
|
||||
<publisher:PayloadEditorControl Grid.Row="6" Grid.Column="1" Payload="{Binding Payload}" FixedJson="True" Height="350" x:Name="PayloadEditor"/>
|
||||
</controls:GridLayout>
|
||||
</UserControl>
|
||||
|
@ -8,10 +8,13 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
/// </summary>
|
||||
public partial class TapetiPublisherView
|
||||
{
|
||||
// ReSharper disable once SuggestBaseTypeForParameterInConstructor - the XAML explicitly requires TapetiPublisherViewModel
|
||||
public TapetiPublisherView(TapetiPublisherViewModel viewModel)
|
||||
{
|
||||
DataContext = viewModel;
|
||||
InitializeComponent();
|
||||
|
||||
PayloadEditor.Validator = viewModel;
|
||||
}
|
||||
|
||||
|
||||
|
@ -5,12 +5,13 @@ using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using PettingZoo.Core.Connection;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.Core.Validation;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
using IConnection = PettingZoo.Core.Connection.IConnection;
|
||||
|
||||
namespace PettingZoo.UI.Tab.Publisher
|
||||
{
|
||||
public class TapetiPublisherViewModel : BaseViewModel, ITabHostWindowNotify
|
||||
public class TapetiPublisherViewModel : BaseViewModel, ITabHostWindowNotify, IPayloadValidator
|
||||
{
|
||||
private readonly IConnection connection;
|
||||
private readonly IPublishDestination publishDestination;
|
||||
@ -23,6 +24,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
private string className = "";
|
||||
private string assemblyName = "";
|
||||
private Window? tabHostWindow;
|
||||
private IValidatingExample? validatingExample;
|
||||
|
||||
|
||||
public string CorrelationId
|
||||
@ -35,7 +37,11 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
public string ClassName
|
||||
{
|
||||
get => string.IsNullOrEmpty(className) ? AssemblyName + "." : className;
|
||||
set => SetField(ref className, value);
|
||||
set
|
||||
{
|
||||
if (SetField(ref className, value))
|
||||
validatingExample = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -123,6 +129,8 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
case IClassTypeExample classTypeExample:
|
||||
AssemblyName = classTypeExample.AssemblyName;
|
||||
ClassName = classTypeExample.FullClassName;
|
||||
|
||||
validatingExample = classTypeExample as IValidatingExample;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -139,8 +147,6 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
|
||||
// TODO background worker / async
|
||||
|
||||
connection.Publish(new PublishMessageInfo(
|
||||
publishDestination.Exchange,
|
||||
publishDestination.RoutingKey,
|
||||
@ -168,6 +174,18 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
{
|
||||
tabHostWindow = hostWindow;
|
||||
}
|
||||
|
||||
|
||||
public bool CanValidate()
|
||||
{
|
||||
return validatingExample != null && validatingExample.CanValidate();
|
||||
}
|
||||
|
||||
|
||||
public void Validate(string validatePayload)
|
||||
{
|
||||
validatingExample?.Validate(validatePayload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user