1
0
mirror of synced 2024-11-21 20:33:49 +00:00

Merge branch 'release/1.1'

This commit is contained in:
Mark van Renswoude 2022-01-05 19:01:36 +01:00
commit 0a7a776646
42 changed files with 870 additions and 119 deletions

View File

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

View File

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

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

View File

@ -1,6 +1,6 @@
using System;
namespace PettingZoo.UI
namespace PettingZoo.Core.Validation
{
public readonly struct TextPosition : IEquatable<TextPosition>
{

View File

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

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

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

View File

@ -27,6 +27,7 @@ namespace PettingZoo.Tapeti.NuGet
public NuGetPackageManager(ILogger logger)
{
this.logger = logger;
cache = new SourceCacheContext();
sources = new List<Source>
{

View File

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

View File

@ -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 () =>

View File

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

View File

@ -78,6 +78,24 @@ namespace PettingZoo.Tapeti.UI.ClassSelection {
}
}
/// <summary>
/// Looks up a localized string similar to Only classes ending in &quot;Message&quot;.
/// </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>

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ namespace PettingZoo.Test.Tapeti
objectValue.Should().HaveElement("RecursiveValue").Which.Type.Should().Be(JTokenType.Null);
// Via type mapping
// TODO
// TODO test type mappings
}
}

View File

@ -11,5 +11,4 @@ Should-have
Nice-to-have
------------
- Validation against message classes (for Tapeti messages)
------------

View File

@ -2,6 +2,7 @@
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
using PettingZoo.Core.Validation;
namespace PettingZoo.UI
{

View File

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

View File

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

View File

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

View File

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

View File

@ -29,11 +29,10 @@
<Border Style="{StaticResource ControlBorder}" Name="EditorBorder">
<avalonedit:TextEditor
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
Name="Editor"
SyntaxHighlighting="{Binding SyntaxHighlighting}"
Style="{StaticResource Payload}"
/>
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
Name="Editor"
SyntaxHighlighting="{Binding SyntaxHighlighting}"
Style="{StaticResource Payload}" />
</Border>
</DockPanel>
</UserControl>

View File

@ -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()
@ -146,6 +154,8 @@ namespace PettingZoo.UI.Tab.Publisher
// Avalon doesn't play nice with bindings it seems:
// https://stackoverflow.com/questions/18964176/two-way-binding-to-avalonedit-document-text-using-mvvm
// ...this is intended though, and well explained here:
// https://github.com/icsharpcode/AvalonEdit/issues/84
Editor.Document.Text = Payload;
@ -181,7 +191,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;

View File

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

View File

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

View File

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

View File

@ -216,12 +216,34 @@ namespace PettingZoo.UI.Tab.Publisher
{
return string.IsNullOrEmpty(value) ? null : value;
}
// TODO check parsing of priority and timestamp
var headers = Headers.Where(h => h.IsValid()).ToDictionary(h => h.Key, h => h.Value);
// TODO background worker / async
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);
connection.Publish(new PublishMessageInfo(
publishDestination.Exchange,
@ -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)
}));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace PettingZoo.UI.Tab.Subscriber
{
public class SameMessageVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return ReferenceEquals(values[0], values[1])
? Visibility.Visible
: Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@ -6,11 +6,15 @@
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:res="clr-namespace:PettingZoo.UI.Tab.Subscriber"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimeSubscriberViewModel, IsDesignTimeCreatable=True}"
Background="White">
<UserControl.Resources>
<res:SameMessageVisibilityConverter x:Key="SameMessageVisibilityConverter" />
</UserControl.Resources>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@ -22,7 +26,8 @@
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding Messages}"
SelectedItem="{Binding Path=SelectedMessage, Mode=TwoWay}"
ui:ListBox.AutoScroll="True">
ui:ListBox.AutoScroll="True"
x:Name="ReferenceControlForBorder">
<ListBox.Resources>
<ui:BindingProxy x:Key="ContextMenuProxy" Data="{Binding}" />
</ListBox.Resources>
@ -30,11 +35,37 @@
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="150"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" MinWidth="150" SharedSizeGroup="DateTime"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding ReceivedTimestamp, StringFormat=g}" Style="{StaticResource Timestamp}"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}"></TextBlock>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- TODO insert as non-focusable item instead, so it's not part of the selection (and perhaps also fixes the bug mentioned in SubscriberViewModel) -->
<Grid Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="DateTime" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Visibility>
<MultiBinding Converter="{StaticResource SameMessageVisibilityConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="DataContext" />
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ListBox}}" Path="DataContext.NewMessage" />
</MultiBinding>
</Grid.Visibility>
<Separator Grid.Column="0" Margin="0,0,8,0" />
<TextBlock Grid.Column="1" Text="{x:Static res:SubscriberViewStrings.LabelNewMessages}" HorizontalAlignment="Center" Background="{Binding Background, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Foreground="{x:Static SystemColors.GrayTextBrush}" />
<Separator Grid.Column="2" Margin="8,0,0,0" />
</Grid>
<TextBlock Grid.Column="0" Grid.Row="1" Text="{Binding ReceivedTimestamp, StringFormat=g}" Style="{StaticResource Timestamp}" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}" />
<Grid.ContextMenu>
<ContextMenu>
@ -66,12 +97,11 @@
<Border Grid.Column="0" Grid.Row="1" Style="{StaticResource SidePanel}">
<DockPanel>
<Label DockPanel.Dock="Top" Style="{StaticResource HeaderLabel}" Content="{x:Static res:SubscriberViewStrings.PanelTitleBody}"/>
<TextBox
Text="{Binding SelectedMessageBody, Mode=OneWay}"
BorderThickness="0"
IsReadOnly="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"/>
<Border Style="{StaticResource ControlBorder}" Name="EditorBorder">
<avalonedit:TextEditor
Name="Editor"
Style="{StaticResource Payload}" />
</Border>
</DockPanel>
</Border>
<GridSplitter HorizontalAlignment="Stretch" Grid.Column="0" Grid.Row="2" Height="5" ResizeDirection="Rows"/>

View File

@ -1,6 +1,7 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Highlighting;
namespace PettingZoo.UI.Tab.Subscriber
{
@ -14,6 +15,22 @@ namespace PettingZoo.UI.Tab.Subscriber
DataContext = viewModel;
InitializeComponent();
// TODO wrap all this nonsense (which is a duplicate from PayloadEditorControl) in a UserControl
// should contain the border, one- or two-way Document binding and the automatic syntax highlighting based on a bound content-type
EditorBorder.BorderBrush = ReferenceControlForBorder.BorderBrush;
Editor.Options.IndentationSize = 2;
viewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName != nameof(viewModel.SelectedMessageBody))
return;
Editor.Document.Text = viewModel.SelectedMessageBody;
Editor.SyntaxHighlighting = (viewModel.SelectedMessage?.Properties.ContentType ?? "") == "application/json"
? HighlightingManager.Instance.GetDefinition(@"Json")
: null;
};
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
Background = Brushes.Transparent;

View File

@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Rendering;
using PettingZoo.WPF.ViewModel;
// TODO visual hint of where the last read message was when activating the tab again
// TODO if the "New message" line is visible when this tab is undocked, the line in the ListBox does not shrink. Haven't been able to figure out yet how to solve it
namespace PettingZoo.UI.Tab.Subscriber
{
@ -18,7 +20,7 @@ namespace PettingZoo.UI.Tab.Subscriber
private readonly ITabFactory tabFactory;
private readonly IConnection connection;
private readonly ISubscriber subscriber;
private readonly TaskScheduler uiScheduler;
private readonly Dispatcher dispatcher;
private ReceivedMessageInfo? selectedMessage;
private readonly DelegateCommand clearCommand;
private readonly TabToolbarCommand[] toolbarCommands;
@ -27,6 +29,8 @@ namespace PettingZoo.UI.Tab.Subscriber
private readonly DelegateCommand createPublisherCommand;
private bool tabActive;
private ReceivedMessageInfo? newMessage;
private Timer? newMessageTimer;
private int unreadCount;
@ -47,6 +51,14 @@ namespace PettingZoo.UI.Tab.Subscriber
}
}
public ReceivedMessageInfo? NewMessage
{
get => newMessage;
set => SetField(ref newMessage, value);
}
public string SelectedMessageBody =>
SelectedMessage != null
? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.Properties.ContentType)
@ -70,8 +82,8 @@ namespace PettingZoo.UI.Tab.Subscriber
this.tabFactory = tabFactory;
this.connection = connection;
this.subscriber = subscriber;
uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
dispatcher = Dispatcher.CurrentDispatcher;
Messages = new ObservableCollection<ReceivedMessageInfo>();
clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
@ -116,12 +128,14 @@ namespace PettingZoo.UI.Tab.Subscriber
private void SubscriberMessageReceived(object? sender, MessageReceivedEventArgs args)
{
RunFromUiScheduler(() =>
dispatcher.BeginInvoke(() =>
{
if (!tabActive)
{
unreadCount++;
RaisePropertyChanged(nameof(Title));
NewMessage ??= args.MessageInfo;
}
Messages.Add(args.MessageInfo);
@ -140,22 +154,39 @@ namespace PettingZoo.UI.Tab.Subscriber
}
private void RunFromUiScheduler(Action action)
{
_ = Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
}
public void Activate()
{
tabActive = true;
unreadCount = 0;
RaisePropertyChanged(nameof(Title));
if (NewMessage == null)
return;
newMessageTimer?.Dispose();
newMessageTimer = new Timer(
_ =>
{
dispatcher.BeginInvoke(() =>
{
NewMessage = null;
});
},
null,
TimeSpan.FromSeconds(5),
Timeout.InfiniteTimeSpan);
}
public void Deactivate()
{
if (newMessageTimer != null)
{
newMessageTimer.Dispose();
newMessageTimer = null;
}
NewMessage = null;
tabActive = false;
}
}
@ -165,6 +196,20 @@ namespace PettingZoo.UI.Tab.Subscriber
{
public DesignTimeSubscriberViewModel() : base(null!, null!, null!, new DesignTimeSubscriber())
{
for (var i = 1; i <= 5; i++)
Messages.Add(new ReceivedMessageInfo(
"designtime",
$"designtime.message.{i}",
Encoding.UTF8.GetBytes(@"Design-time message"),
new MessageProperties(null)
{
ContentType = "text/fake",
ReplyTo = "/dev/null"
},
DateTime.Now));
SelectedMessage = Messages[2];
NewMessage = Messages[2];
}

View File

@ -96,6 +96,15 @@ namespace PettingZoo.UI.Tab.Subscriber {
}
}
/// <summary>
/// Looks up a localized string similar to New messages.
/// </summary>
public static string LabelNewMessages {
get {
return ResourceManager.GetString("LabelNewMessages", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Body.
/// </summary>

View File

@ -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="CommandClear" xml:space="preserve">
<value>Clear</value>
@ -129,6 +129,9 @@
<data name="DeliveryModePersistent" xml:space="preserve">
<value>Persistent</value>
</data>
<data name="LabelNewMessages" xml:space="preserve">
<value>New messages</value>
</data>
<data name="PanelTitleBody" xml:space="preserve">
<value>Body</value>
</data>

View File

@ -8,7 +8,7 @@ using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Tab.Undocked
{
public class UndockedTabHostViewModel : BaseViewModel
public class UndockedTabHostViewModel : BaseViewModel, ITabActivate
{
private readonly ITabHost tabHost;
private readonly ITab tab;
@ -51,6 +51,17 @@ namespace PettingZoo.UI.Tab.Undocked
{
tabHost.UndockedTabClosed(tab);
}
public void Activate()
{
(tab as ITabActivate)?.Activate();
}
public void Deactivate()
{
(tab as ITabActivate)?.Deactivate();
}
}

View File

@ -1,5 +1,7 @@
using System.Windows;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace PettingZoo.UI.Tab.Undocked
{
@ -30,6 +32,16 @@ namespace PettingZoo.UI.Tab.Undocked
{
viewModel.WindowClosed();
};
Activated += (_, _) =>
{
viewModel.Activate();
};
Deactivated += (_, _) =>
{
viewModel.Deactivate();
};
}
private void Toolbar_Loaded(object sender, RoutedEventArgs e)