Merge branch 'release/1.0'

This commit is contained in:
Mark van Renswoude 2022-01-01 21:45:29 +01:00
commit dd9f089602
87 changed files with 3033 additions and 560 deletions

View File

@ -1,25 +1,33 @@
using System;
using System.Collections.Generic;
namespace PettingZoo.Core.Generator
{
public interface IExampleSource : IDisposable
public interface IExampleGenerator
{
IExampleFolder GetRootFolder();
void Select(object? ownerWindow, Action<IExample> onExampleSelected);
}
public interface IExampleFolder
{
public string Name { get; }
public IReadOnlyList<IExampleFolder> Folders { get; }
public IReadOnlyList<IExampleMessage> Messages { get; }
}
public interface IExampleMessage
public interface IExample
{
string Generate();
}
public interface IClassTypeExample : IExample
{
public string AssemblyName { get; }
public string? Namespace { get; }
public string ClassName { get; }
public string FullClassName => !string.IsNullOrEmpty(Namespace) ? Namespace + "." : "" + ClassName;
}
/*
public interface IValidatingExample : IExample
{
bool Validate(string payload, out string validationMessage);
}
*/
}

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>

View File

@ -7,7 +7,7 @@ namespace PettingZoo.Core.Rendering
{
public class MessageBodyRenderer
{
public static Dictionary<string, Func<byte[], string>> ContentTypeHandlers = new()
private static readonly Dictionary<string, Func<byte[], string>> ContentTypeHandlers = new()
{
{ "application/json", RenderJson }
};
@ -15,7 +15,7 @@ namespace PettingZoo.Core.Rendering
public static string Render(byte[] body, string? contentType)
{
return (contentType != null) && ContentTypeHandlers.TryGetValue(contentType, out var handler)
return contentType != null && ContentTypeHandlers.TryGetValue(contentType, out var handler)
? handler(body)
: Encoding.UTF8.GetString(body);

View File

@ -0,0 +1,36 @@
using System;
using System.IO;
using System.Reflection;
namespace PettingZoo.Core.Settings
{
public static class PettingZooPaths
{
public static string AppDataRoot { get; }
public static string InstallationRoot { get; }
public static string LogPath => Path.Combine(AppDataRoot, @"Logs");
public static string DatabasePath => AppDataRoot;
public const string AssembliesPath = @"Assemblies";
public static string AppDataAssemblies => Path.Combine(AppDataRoot, AssembliesPath);
public static string InstallationAssemblies => Path.Combine(InstallationRoot, AssembliesPath);
static PettingZooPaths()
{
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
if (appDataPath == null)
throw new IOException("Could not resolve application data path");
AppDataRoot = Path.Combine(appDataPath, @"PettingZoo");
if (!Directory.CreateDirectory(AppDataRoot).Exists)
throw new IOException($"Failed to create directory: {AppDataRoot}");
InstallationRoot = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location ?? Assembly.GetExecutingAssembly().Location)!;
}
}
}

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,5 +1,6 @@
using LiteDB;
using LiteDB.Async;
using PettingZoo.Core.Settings;
namespace PettingZoo.Settings.LiteDB
{
@ -15,15 +16,7 @@ namespace PettingZoo.Settings.LiteDB
public BaseLiteDBRepository(string databaseName)
{
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
if (appDataPath == null)
throw new IOException("Could not resolve application data path");
var databasePath = Path.Combine(appDataPath, @"PettingZoo");
if (!Directory.CreateDirectory(databasePath).Exists)
throw new IOException($"Failed to create directory: {databasePath}");
databaseFilename = Path.Combine(databasePath, $"{databaseName}.litedb");
databaseFilename = Path.Combine(PettingZooPaths.DatabasePath, $"{databaseName}.litedb");
}

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.11" />
<PackageReference Include="LiteDB.Async" Version="0.0.11" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace PettingZoo.Tapeti.AssemblyLoader
{
public class FilePackageAssemblies : IPackageAssemblies
{
private readonly string[] filenames;
public FilePackageAssemblies(params string[] filenames)
{
this.filenames = filenames;
}
public Task<IEnumerable<IPackageAssembly>> GetAssemblies(IProgress<int> progress, CancellationToken cancellationToken)
{
return Task.FromResult(filenames.Select(f => (IPackageAssembly)new FilePackageAssembly(f)));
}
private class FilePackageAssembly : IPackageAssembly
{
private readonly string filename;
public FilePackageAssembly(string filename)
{
this.filename = filename;
}
public Stream GetStream()
{
return new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace PettingZoo.Tapeti.AssemblyLoader
{
public interface IPackageAssemblies
{
Task<IEnumerable<IPackageAssembly>> GetAssemblies(IProgress<int> progress, CancellationToken cancellationToken);
}
public interface IPackageAssembly
{
Stream GetStream();
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Frameworks;
using NuGet.Packaging;
using PettingZoo.Tapeti.NuGet;
namespace PettingZoo.Tapeti.AssemblyLoader
{
public class NuGetPackageAssemblies : IPackageAssemblies
{
private readonly INuGetPackageVersion packageVersion;
public NuGetPackageAssemblies(INuGetPackageVersion packageVersion)
{
this.packageVersion = packageVersion;
}
public async Task<IEnumerable<IPackageAssembly>> GetAssemblies(IProgress<int> progress, CancellationToken cancellationToken)
{
await using var packageStream = new MemoryStream();
await packageVersion.Download(packageStream, cancellationToken);
packageStream.Seek(0, SeekOrigin.Begin);
using var packageReader = new PackageArchiveReader(packageStream);
// Determine which frameworks versions PettingZoo is compatible with so that we can actually load the assemblies
var targetFrameworkAttribute = Assembly.GetExecutingAssembly()
.GetCustomAttributes(typeof(TargetFrameworkAttribute), false)
.Cast<TargetFrameworkAttribute>()
.Single();
var targetFramework = NuGetFramework.ParseFrameworkName(targetFrameworkAttribute.FrameworkName, DefaultFrameworkNameProvider.Instance);
var libVersion = (await packageReader.GetLibItemsAsync(cancellationToken))
.Where(l => DefaultCompatibilityProvider.Instance.IsCompatible(targetFramework, l.TargetFramework))
.OrderByDescending(l => l.TargetFramework)
.FirstOrDefault();
if (libVersion == null)
return Enumerable.Empty<IPackageAssembly>();
var assemblies = new List<IPackageAssembly>();
foreach (var filename in libVersion.Items.Where(f => f.EndsWith(@".dll", StringComparison.InvariantCultureIgnoreCase)))
{
var assembly = await new NuGetPackageAssembly().CopyFrom(packageReader.GetStream(filename));
assemblies.Add(assembly);
}
return assemblies;
}
private class NuGetPackageAssembly : IPackageAssembly
{
private readonly MemoryStream buffer = new();
public async Task<IPackageAssembly> CopyFrom(Stream stream)
{
await stream.CopyToAsync(buffer);
return this;
}
public Stream GetStream()
{
return new MemoryStream(buffer.GetBuffer());
}
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Loader;
using Newtonsoft.Json;
using PettingZoo.Core.Generator;
namespace PettingZoo.Tapeti.AssemblyParser
{
public class AssemblyParser : IDisposable
{
private readonly 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);
}
}
public void Dispose()
{
loadContext.Unload();
GC.SuppressFinalize(this);
}
public IEnumerable<IClassTypeExample> GetExamples(Stream assemblyStream)
{
var assembly = loadContext.LoadFromStream(assemblyStream);
foreach (var type in assembly.GetTypes().Where(t => t.IsClass))
yield return new TypeExample(type);
}
private class TypeExample : IClassTypeExample
{
public string AssemblyName => type.Assembly.GetName().Name ?? "";
public string? Namespace => type.Namespace;
public string ClassName => type.Name;
private readonly Type type;
public TypeExample(Type type)
{
this.type = type;
}
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);
}
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace PettingZoo.Tapeti.NuGet
{
public interface INuGetPackageManager
{
public IReadOnlyList<INuGetPackageSource> Sources { get; }
}
public interface INuGetPackageSource
{
public string Name { get; }
public Task<IReadOnlyList<INuGetPackage>> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken);
}
public interface INuGetPackage
{
public string Title { get; }
public string Description { get; }
public string Authors { get; }
public string Version { get; }
public Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken);
}
public interface INuGetPackageVersion : IComparable<INuGetPackageVersion>
{
public string Version { get; }
public Task Download(Stream destination, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using NuGet.Common;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using ILogger = Serilog.ILogger;
namespace PettingZoo.Tapeti.NuGet
{
public class NuGetPackageManager : INuGetPackageManager
{
private const string NuGetDefaultSource = @"https://api.nuget.org/v3/index.json";
private readonly ILogger logger;
private readonly SourceCacheContext cache;
private readonly List<Source> sources;
public IReadOnlyList<INuGetPackageSource> Sources => sources;
public NuGetPackageManager(ILogger logger)
{
this.logger = logger;
cache = new SourceCacheContext();
sources = new List<Source>
{
new(logger.ForContext("source", NuGetDefaultSource), cache, "nuget.org", NuGetDefaultSource)
};
}
public NuGetPackageManager WithSourcesFrom(string nuGetConfig)
{
if (!File.Exists(nuGetConfig))
return this;
var doc = new XmlDocument();
doc.Load(nuGetConfig);
var nodes = doc.SelectNodes(@"/configuration/packageSources/add");
if (nodes == null)
return this;
foreach (var entry in nodes.Cast<XmlNode>())
{
if (entry.Attributes == null)
continue;
var nameAttribute = entry.Attributes["key"];
var urlAttribute = entry.Attributes["value"];
if (string.IsNullOrEmpty(nameAttribute?.Value) || string.IsNullOrEmpty(urlAttribute?.Value))
continue;
sources.Add(new Source(logger.ForContext("source", urlAttribute.Value), cache, nameAttribute.Value, urlAttribute.Value));
}
return this;
}
private class Source : INuGetPackageSource
{
private readonly ILogger logger;
private readonly SourceCacheContext cache;
private readonly SourceRepository repository;
public string Name { get; }
public Source(ILogger logger, SourceCacheContext cache, string name, string url)
{
this.logger = logger;
this.cache = cache;
Name = name;
repository = Repository.Factory.GetCoreV3(url);
}
public async Task<IReadOnlyList<INuGetPackage>> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(searchTerm))
return Array.Empty<INuGetPackage>();
try
{
var resource = await repository.GetResourceAsync<PackageSearchResource>(cancellationToken);
var filter = new SearchFilter(includePrerelease);
var result = (await resource.SearchAsync(searchTerm, filter, 0, 20, new NullLogger(),
cancellationToken))
.Select(p => new Package(logger, cache, repository, p))
.ToArray();
return result;
}
catch (Exception e)
{
logger.Error(e, "NuGet Search failed for term '{searchTerm}' (includePrerelease {includePrerelease})", searchTerm, includePrerelease);
throw;
}
}
}
protected class Package : INuGetPackage
{
private readonly ILogger logger;
private readonly SourceCacheContext cache;
private readonly SourceRepository repository;
private readonly IPackageSearchMetadata packageSearchMetadata;
public string Title => packageSearchMetadata.Title;
public string Description => packageSearchMetadata.Description;
public string Authors => packageSearchMetadata.Authors;
public string Version => packageSearchMetadata.Identity.Version.ToString();
private IReadOnlyList<INuGetPackageVersion>? versions;
public Package(ILogger logger, SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata)
{
this.logger = logger;
this.cache = cache;
this.repository = repository;
this.packageSearchMetadata = packageSearchMetadata;
}
public async Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken)
{
try
{
return versions ??= (await packageSearchMetadata.GetVersionsAsync())
.Select(v => new PackageVersion(cache, repository, packageSearchMetadata, v.Version))
.ToArray();
}
catch (Exception e)
{
logger.Error(e, "NuGet GetVersions failed for packge Id '{packageId}')", packageSearchMetadata.Identity.Id);
throw;
}
}
}
protected class PackageVersion : INuGetPackageVersion, IComparable<PackageVersion>
{
private readonly SourceCacheContext cache;
private readonly SourceRepository repository;
private readonly IPackageSearchMetadata packageSearchMetadata;
protected readonly NuGetVersion NuGetVersion;
public PackageVersion(SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata, NuGetVersion nuGetVersion)
{
this.cache = cache;
this.repository = repository;
this.packageSearchMetadata = packageSearchMetadata;
NuGetVersion = nuGetVersion;
}
public string Version => NuGetVersion.ToString();
public async Task Download(Stream destination, CancellationToken cancellationToken)
{
var resource = await repository.GetResourceAsync<FindPackageByIdResource>(cancellationToken);
await resource.CopyNupkgToStreamAsync(packageSearchMetadata.Identity.Id, NuGetVersion, destination, cache, new NullLogger(), cancellationToken);
}
public int CompareTo(INuGetPackageVersion? other)
{
if (ReferenceEquals(this, other)) return 0;
if (other == null) return 1;
return other is PackageVersion packageVersion ? CompareTo(packageVersion) : string.Compare(Version, other.Version, StringComparison.Ordinal);
}
public int CompareTo(PackageVersion? other)
{
if (ReferenceEquals(this, other)) return 0;
return other == null ? 1 : NuGetVersion.CompareTo(other.NuGetVersion);
}
}
}
}

View File

@ -1,16 +1,70 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>0.1</Version>
<UseWpf>true</UseWpf>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NuGet.Packaging" Version="6.0.0" />
<PackageReference Include="NuGet.Protocol" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="SharpVectors" Version="1.7.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.DataAnnotations.Extensions" Version="3.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PettingZoo.Core\PettingZoo.Core.csproj" />
<ProjectReference Include="..\PettingZoo.WPF\PettingZoo.WPF.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="UI\ClassSelection\ClassSelectionStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ClassSelectionStrings.resx</DependentUpon>
</Compile>
<Compile Update="UI\PackageProgress\PackageProgressStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>PackageProgressStrings.resx</DependentUpon>
</Compile>
<Compile Update="UI\PackageProgress\PackageProgressWindow.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="UI\PackageSelection\PackageSelectionStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>PackageSelectionStrings.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="UI\ClassSelection\ClassSelectionStrings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>ClassSelectionStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="UI\PackageProgress\PackageProgressStrings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>PackageProgressStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="UI\PackageSelection\PackageSelectionStrings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>PackageSelectionStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Page Update="UI\PackageProgress\PackageProgressWindow.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using PettingZoo.Core.Generator;
using PettingZoo.Core.Settings;
using PettingZoo.Tapeti.AssemblyLoader;
using PettingZoo.Tapeti.NuGet;
using PettingZoo.Tapeti.UI.ClassSelection;
using PettingZoo.Tapeti.UI.PackageProgress;
using PettingZoo.Tapeti.UI.PackageSelection;
using Serilog;
namespace PettingZoo.Tapeti
{
public class TapetiClassLibraryExampleGenerator : IExampleGenerator
{
private readonly ILogger logger;
public TapetiClassLibraryExampleGenerator(ILogger logger)
{
this.logger = logger;
}
public void Select(object? ownerWindow, Action<IExample> onExampleSelected)
{
var packageManager = new NuGetPackageManager(logger)
.WithSourcesFrom(Path.Combine(PettingZooPaths.InstallationRoot, @"nuget.config"))
.WithSourcesFrom(Path.Combine(PettingZooPaths.AppDataRoot, @"nuget.config"));
var dispatcher = Dispatcher.CurrentDispatcher;
var viewModel = new PackageSelectionViewModel(packageManager);
var selectionWindow = new PackageSelectionWindow(viewModel)
{
Owner = ownerWindow as Window
};
viewModel.Select += (_, args) =>
{
dispatcher.Invoke(() =>
{
var windowBounds = selectionWindow.RestoreBounds;
selectionWindow.Close();
var progressWindow = new PackageProgressWindow();
progressWindow.Left = windowBounds.Left + (windowBounds.Width - progressWindow.Width) / 2;
progressWindow.Left = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2;
progressWindow.Show();
Task.Run(async () =>
{
try
{
// TODO allow cancelling (by closing the progress window and optionally a Cancel button)
var assemblies = await args.Assemblies.GetAssemblies(progressWindow, CancellationToken.None);
// var classes =
var examples = LoadExamples(assemblies);
dispatcher.Invoke(() =>
{
progressWindow.Close();
progressWindow = null;
var classSelectionViewModel = new ClassSelectionViewModel(examples);
var classSelectionWindow = new ClassSelectionWindow(classSelectionViewModel)
{
Top = windowBounds.Top,
Left = windowBounds.Left,
Width = windowBounds.Width,
Height = windowBounds.Height
};
classSelectionViewModel.Select += (_, example) =>
{
classSelectionWindow.Close();
onExampleSelected(example);
};
classSelectionWindow.ShowDialog();
});
}
catch (Exception e)
{
dispatcher.Invoke(() =>
{
// ReSharper disable once ConstantConditionalAccessQualifier - if I remove it, there's a "Dereference of a possibly null reference" warning instead
progressWindow?.Close();
MessageBox.Show($"Error while loading assembly: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
});
}
});
});
};
selectionWindow.ShowDialog();
}
private static IEnumerable<IClassTypeExample> LoadExamples(IEnumerable<IPackageAssembly> assemblies)
{
var assemblyParser = new AssemblyParser.AssemblyParser(
PettingZooPaths.AppDataAssemblies,
PettingZooPaths.InstallationAssemblies
);
return assemblies
.SelectMany(a =>
{
using var stream = a.GetStream();
return assemblyParser.GetExamples(stream).ToArray();
})
.ToArray();
}
}
}

View File

@ -1,187 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using PettingZoo.Core.Generator;
namespace PettingZoo.Tapeti
{
public class TapetiClassLibraryExampleSource : IExampleSource
{
private readonly string classLibraryFilename;
private readonly IEnumerable<string> extraAssemblies;
private Lazy<AssemblySource> assemblySource;
public TapetiClassLibraryExampleSource(string classLibraryFilename, IEnumerable<string> extraAssemblies)
{
this.classLibraryFilename = classLibraryFilename;
this.extraAssemblies = extraAssemblies;
assemblySource = new Lazy<AssemblySource>(AssemblySourceFactory);
}
public void Dispose()
{
if (assemblySource.IsValueCreated)
assemblySource.Value.Dispose();
GC.SuppressFinalize(this);
}
public IExampleFolder GetRootFolder()
{
return assemblySource.Value.RootFolder;
}
private AssemblySource AssemblySourceFactory()
{
var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");
var paths = runtimeAssemblies
.Concat(extraAssemblies)
.Append(classLibraryFilename);
// TODO can we use a custom resolver to detect missing references?
var resolver = new PathAssemblyResolver(paths);
var loadContext = new MetadataLoadContext(resolver);
try
{
var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename);
var rootFolder = new Folder(@"Root");
foreach (var assemblyType in assembly.GetTypes())
AddType(assemblyType, rootFolder);
return new AssemblySource
{
LoadContext = loadContext,
RootFolder = rootFolder
};
}
catch
{
loadContext.Dispose();
throw;
}
}
private void AddType(Type type, Folder rootFolder)
{
if (!type.IsClass)
return;
var assemblyName = type.Assembly.GetName().Name + ".";
var typeNamespace = type.Namespace ?? "";
if (typeNamespace.StartsWith(assemblyName))
typeNamespace = typeNamespace.Substring(assemblyName.Length);
var folder = CreateFolder(rootFolder, typeNamespace);
folder.AddMessage(new Message(type));
}
private static Folder CreateFolder(Folder rootFolder, string typeNamespace)
{
var parts = typeNamespace.Split('.');
if (parts.Length == 0)
return rootFolder;
var folder = rootFolder;
foreach (var part in parts)
folder = folder.CreateFolder(part);
return folder;
}
private class Folder : IExampleFolder
{
private readonly List<Folder> folders = new();
private readonly List<IExampleMessage> messages = new();
public string Name { get; }
public IReadOnlyList<IExampleFolder> Folders => folders;
public IReadOnlyList<IExampleMessage> Messages => messages;
public Folder(string name)
{
Name = name;
}
public Folder CreateFolder(string name)
{
var folder = folders.FirstOrDefault(f => f.Name == name);
if (folder != null)
return folder;
folder = new Folder(name);
folders.Add(folder);
return folder;
}
public void AddMessage(IExampleMessage message)
{
messages.Add(message);
}
}
private class Message : IExampleMessage
{
private readonly Type type;
public Message(Type type)
{
this.type = type;
}
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);
}
}
private class AssemblySource : IDisposable
{
public MetadataLoadContext LoadContext { get; init; }
public IExampleFolder RootFolder { get; init; }
public void Dispose()
{
LoadContext.Dispose();
}
}
}
}

View File

@ -6,18 +6,47 @@ using Newtonsoft.Json.Linq;
namespace PettingZoo.Tapeti
{
internal class TypeToJObjectConverter
public class TypeToJObjectConverter
{
public static JObject Convert(Type type)
{
if (!type.IsClass)
throw new ArgumentException($"TypeToJObjectConverter.Convert expects a class, got {type.Name}");
return ClassToJToken(type, Array.Empty<Type>());
}
private static readonly Dictionary<Type, Type> TypeEquivalenceMap = new()
{
{ typeof(uint), typeof(int) },
{ typeof(long), typeof(int) },
{ typeof(ulong), typeof(int) },
{ typeof(short), typeof(int) },
{ typeof(ushort), typeof(int) },
{ typeof(float), typeof(decimal) }
};
private static readonly Dictionary<Type, JToken> TypeValueMap = new()
{
{ typeof(int), 0 },
{ typeof(decimal), 0.0 },
{ typeof(bool), false }
};
private static JObject ClassToJToken(Type classType, IEnumerable<Type> typesEncountered)
{
var newTypesEncountered = typesEncountered.Append(classType).ToArray();
var result = new JObject();
foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
foreach (var propertyInfo in classType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Note: unfortunately we can not call GetCustomAttributes here, as that would
// trigger assemblies not included in the package to be loaded
// 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 = PropertyToJToken(propertyInfo.PropertyType);
var value = TypeToJToken(propertyInfo.PropertyType, newTypesEncountered);
result.Add(propertyInfo.Name, value);
}
@ -25,23 +54,12 @@ namespace PettingZoo.Tapeti
}
private static readonly Dictionary<Type, JToken> TypeMap = new()
private static JToken TypeToJToken(Type type, ICollection<Type> typesEncountered)
{
{ typeof(short), 0 },
{ typeof(ushort), 0 },
{ typeof(int), 0 },
{ typeof(uint), 0 },
{ typeof(long), 0 },
{ typeof(ulong), 0 },
{ typeof(decimal), 0.0 },
{ typeof(float), 0.0 },
{ typeof(bool), false }
};
var actualType = Nullable.GetUnderlyingType(type) ?? type;
private static JToken PropertyToJToken(Type propertyType)
{
var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
if (TypeEquivalenceMap.TryGetValue(actualType, out var equivalentType))
actualType = equivalentType;
// String is also a class
@ -56,14 +74,13 @@ namespace PettingZoo.Tapeti
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
if (enumerableInterface != null)
return new JArray(Convert(enumerableInterface.GetGenericArguments()[0]));
return new JArray(TypeToJToken(enumerableInterface.GetGenericArguments()[0], typesEncountered));
return Convert(actualType);
return typesEncountered.Contains(actualType) ? new JValue((object?)null) : ClassToJToken(actualType, typesEncountered);
}
if (actualType.IsArray)
return new JArray(Convert(actualType.GetElementType()));
return new JArray(TypeToJToken(actualType.GetElementType()!, typesEncountered));
if (actualType.IsEnum)
return Enum.GetNames(actualType).FirstOrDefault();
@ -80,11 +97,9 @@ namespace PettingZoo.Tapeti
if (actualType == typeof(Guid))
return Guid.NewGuid().ToString();
return TypeMap.TryGetValue(actualType, out var mappedToken)
return TypeValueMap.TryGetValue(actualType, out var mappedToken)
? mappedToken
: $"(unknown type: {actualType.Name})";
}
}
}

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.UI.ClassSelection {
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()]
public class ClassSelectionStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal ClassSelectionStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.UI.ClassSelection.ClassSelectionStrings", typeof(ClassSelectionStrings).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)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string ButtonCancel {
get {
return ResourceManager.GetString("ButtonCancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select.
/// </summary>
public static string ButtonSelect {
get {
return ResourceManager.GetString("ButtonSelect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select class.
/// </summary>
public static string WindowTitle {
get {
return ResourceManager.GetString("WindowTitle", 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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ButtonCancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ButtonSelect" xml:space="preserve">
<value>Select</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Select class</value>
</data>
</root>

View File

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using PettingZoo.Core.Generator;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.Tapeti.UI.ClassSelection
{
public class ClassSelectionViewModel : BaseViewModel
{
private BaseClassTreeItem? selectedItem;
private readonly DelegateCommand selectCommand;
public ObservableCollection<BaseClassTreeItem> Examples { get; } = new();
public BaseClassTreeItem? SelectedItem
{
get => selectedItem;
set
{
if (!SetField(ref selectedItem, value))
return;
selectCommand.RaiseCanExecuteChanged();
}
}
public ICommand SelectCommand => selectCommand;
public event EventHandler<IExample>? Select;
public ClassSelectionViewModel(IEnumerable<IClassTypeExample> examples)
{
selectCommand = new DelegateCommand(SelectExecute, SelectCanExecute);
TreeFromExamples(examples);
}
private void SelectExecute()
{
if (SelectedItem is not ExampleTreeItem exampleTreeItem)
return;
Select?.Invoke(this, exampleTreeItem.Example);
}
private bool SelectCanExecute()
{
return SelectedItem is ExampleTreeItem;
}
private void TreeFromExamples(IEnumerable<IClassTypeExample> examples)
{
var root = new NamespaceFolderClassTreeItem(string.Empty);
foreach (var example in examples)
{
var folder = !string.IsNullOrEmpty(example.Namespace)
? CreateFolder(root, example.Namespace.Split('.'))
: root;
folder.AddChild(new ExampleTreeItem(example));
}
// If the first levels only consist of one child folder, collapse them into one entry
var collapsedRoot = root;
while (collapsedRoot.Children.Count == 1 && collapsedRoot.Children.First() is NamespaceFolderClassTreeItem newRoot)
collapsedRoot = newRoot.Collapse(collapsedRoot.Name);
if (ReferenceEquals(collapsedRoot, root))
{
foreach (var rootItem in root.Children)
Examples.Add(rootItem);
}
else
Examples.Add(collapsedRoot);
}
private static NamespaceFolderClassTreeItem CreateFolder(NamespaceFolderClassTreeItem root, IEnumerable<string> parts)
{
var parent = root;
foreach (var part in parts)
{
if (parent.Children.FirstOrDefault(c => c is NamespaceFolderClassTreeItem && c.Name == part) is NamespaceFolderClassTreeItem child)
{
parent = child;
continue;
}
child = new NamespaceFolderClassTreeItem(part);
parent.AddChild(child);
parent = child;
}
return parent;
}
}
public class BaseClassTreeItem
{
private readonly SortedSet<BaseClassTreeItem> children = new(new BaseClassTreeItemComparer());
public string Name { get; protected set; }
public IReadOnlyCollection<BaseClassTreeItem> Children => children;
public BaseClassTreeItem(string name)
{
Name = name;
}
public void AddChild(BaseClassTreeItem item)
{
children.Add(item);
}
}
public class NamespaceFolderClassTreeItem : BaseClassTreeItem
{
public NamespaceFolderClassTreeItem(string name) : base(name)
{
}
public NamespaceFolderClassTreeItem Collapse(string parentFolderName)
{
Name = string.IsNullOrEmpty(parentFolderName) ? Name : parentFolderName + "." + Name;
return this;
}
}
public class ExampleTreeItem : BaseClassTreeItem
{
public IClassTypeExample Example { get; }
public ExampleTreeItem(IClassTypeExample example) : base(example.ClassName)
{
Example = example;
}
}
public class BaseClassTreeItemComparer : IComparer<BaseClassTreeItem>
{
public int Compare(BaseClassTreeItem? x, BaseClassTreeItem? y)
{
if (ReferenceEquals(x, y)) return 0;
if (y == null) return 1;
if (x == null) return -1;
if (x.GetType() != y.GetType())
return x is NamespaceFolderClassTreeItem ? -1 : 1;
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
}
public class DesignTimeClassSelectionViewModel : ClassSelectionViewModel
{
public DesignTimeClassSelectionViewModel()
: base(new IClassTypeExample[]
{
new DesignTimeExample("Messaging.Test", "Messaging.Test", "TestMessage"),
new DesignTimeExample("Messaging.Test", "Messaging.Test", "SomeRequestMessage"),
new DesignTimeExample("Messaging.Test", "Messaging.Test.Model", "SomeViewModel")
})
{
}
private class DesignTimeExample : IClassTypeExample
{
public string AssemblyName { get; }
public string? Namespace { get; }
public string ClassName { get; }
public DesignTimeExample(string assemblyName, string? ns, string className)
{
AssemblyName = assemblyName;
Namespace = ns;
ClassName = className;
}
public string Generate()
{
return "";
}
}
}
}

View File

@ -0,0 +1,69 @@
<Window x:Class="PettingZoo.Tapeti.UI.ClassSelection.ClassSelectionWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
xmlns:classSelection="clr-namespace:PettingZoo.Tapeti.UI.ClassSelection"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
Title="{x:Static classSelection:ClassSelectionStrings.WindowTitle}"
Height="600"
Width="800"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterOwner"
d:DataContext="{d:DesignInstance classSelection:DesignTimeClassSelectionViewModel, IsDesignTimeCreatable=True}">
<Window.Resources>
<ResourceDictionary>
<Style x:Key="TreeItemIcon" TargetType="{x:Type Image}">
<Setter Property="Margin" Value="2,2,8,2" />
</Style>
<Style x:Key="TreeItemLabel" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/PettingZoo.WPF;component/Style.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<controls:GridLayout Style="{StaticResource Form}" Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TreeView Grid.Row="0" ItemsSource="{Binding Examples}" SelectedItemChanged="TreeView_OnSelectedItemChanged">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type classSelection:NamespaceFolderClassTreeItem}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<!--
I couldn't get the image assets to work from within this assembly, so I've simply included them in the main application.
Not pretty, and it still doesn't show up in design-time, but works at runtime for now.
-->
<Image Source="{svgc:SvgImage Source=/Images/Folder.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource TreeItemIcon}"/>
<TextBlock Text="{Binding Name}" Style="{StaticResource TreeItemLabel}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type classSelection:ExampleTreeItem}">
<StackPanel Orientation="Horizontal">
<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>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
<StackPanel Grid.Row="1" 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}" />
</StackPanel>
</controls:GridLayout>
</Window>

View File

@ -0,0 +1,27 @@
using System.Windows;
namespace PettingZoo.Tapeti.UI.ClassSelection
{
/// <summary>
/// Interaction logic for ClassSelectionWindow.xaml
/// </summary>
public partial class ClassSelectionWindow
{
private readonly ClassSelectionViewModel viewModel;
public ClassSelectionWindow(ClassSelectionViewModel viewModel)
{
this.viewModel = viewModel;
DataContext = viewModel;
InitializeComponent();
}
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
viewModel.SelectedItem = (BaseClassTreeItem)e.NewValue;
}
}
}

View File

@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Example {
namespace PettingZoo.Tapeti.UI.PackageProgress {
using System;
@ -22,14 +22,14 @@ namespace PettingZoo.UI.Example {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class ExamplePickerDialogStrings {
public class PackageProgressStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal ExamplePickerDialogStrings() {
internal PackageProgressStrings() {
}
/// <summary>
@ -39,7 +39,7 @@ namespace PettingZoo.UI.Example {
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Example.ExamplePickerDialogStrings", typeof(ExamplePickerDialogStrings).Assembly);
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.UI.PackageProgress.PackageProgressStrings", typeof(PackageProgressStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
@ -61,7 +61,7 @@ namespace PettingZoo.UI.Example {
}
/// <summary>
/// Looks up a localized string similar to Select example.
/// Looks up a localized string similar to Reading message classes....
/// </summary>
public static string WindowTitle {
get {

View File

@ -118,6 +118,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="WindowTitle" xml:space="preserve">
<value>Select example</value>
<value>Reading message classes...</value>
</data>
</root>

View File

@ -0,0 +1,14 @@
<Window x:Class="PettingZoo.Tapeti.UI.PackageProgress.PackageProgressWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:packageProgress="clr-namespace:PettingZoo.Tapeti.UI.PackageProgress"
mc:Ignorable="d"
Height="80"
Width="400"
Title="{x:Static packageProgress:PackageProgressStrings.WindowTitle}"
ResizeMode="NoResize"
WindowStyle="ToolWindow">
<ProgressBar Height="25" Margin="16" VerticalAlignment="Center" Name="Progress" Maximum="100" />
</Window>

View File

@ -0,0 +1,24 @@
using System;
namespace PettingZoo.Tapeti.UI.PackageProgress
{
/// <summary>
/// Interaction logic for PackageProgressWindow.xaml
/// </summary>
public partial class PackageProgressWindow : IProgress<int>
{
public PackageProgressWindow()
{
InitializeComponent();
}
public void Report(int value)
{
Dispatcher.BeginInvoke(() =>
{
Progress.Value = value;
});
}
}
}

View File

@ -0,0 +1,171 @@
//------------------------------------------------------------------------------
// <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.UI.PackageSelection {
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()]
public class PackageSelectionStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal PackageSelectionStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.UI.PackageSelection.PackageSelectionStrings", typeof(PackageSelectionStrings).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)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Assembly files (*.dll)|*.dll|All files (*.*)|*.*.
/// </summary>
public static string AssemblyFileFilter {
get {
return ResourceManager.GetString("AssemblyFileFilter", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Browse....
/// </summary>
public static string ButtonBrowse {
get {
return ResourceManager.GetString("ButtonBrowse", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string ButtonCancel {
get {
return ResourceManager.GetString("ButtonCancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select.
/// </summary>
public static string ButtonSelect {
get {
return ResourceManager.GetString("ButtonSelect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Include prerelease.
/// </summary>
public static string CheckPrerelease {
get {
return ResourceManager.GetString("CheckPrerelease", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You can add extra NuGet feeds by placing a standard nuget.config file in PettingZoo&apos;s installation directory &quot;{0}&quot; or in &quot;{1}&quot;.
/// </summary>
public static string HintNuGetSources {
get {
return ResourceManager.GetString("HintNuGetSources", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version.
/// </summary>
public static string LabelVersion {
get {
return ResourceManager.GetString("LabelVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loading....
/// </summary>
public static string Loading {
get {
return ResourceManager.GetString("Loading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search....
/// </summary>
public static string PlaceholderNuGetSearch {
get {
return ResourceManager.GetString("PlaceholderNuGetSearch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Load from assembly file.
/// </summary>
public static string RadioAssembly {
get {
return ResourceManager.GetString("RadioAssembly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Load from NuGet package.
/// </summary>
public static string RadioNuGet {
get {
return ResourceManager.GetString("RadioNuGet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select source.
/// </summary>
public static string WindowTitle {
get {
return ResourceManager.GetString("WindowTitle", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AssemblyFileFilter" xml:space="preserve">
<value>Assembly files (*.dll)|*.dll|All files (*.*)|*.*</value>
</data>
<data name="ButtonBrowse" xml:space="preserve">
<value>Browse...</value>
</data>
<data name="ButtonCancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ButtonSelect" xml:space="preserve">
<value>Select</value>
</data>
<data name="CheckPrerelease" xml:space="preserve">
<value>Include prerelease</value>
</data>
<data name="HintNuGetSources" xml:space="preserve">
<value>You can add extra NuGet feeds by placing a standard nuget.config file in PettingZoo's installation directory "{0}" or in "{1}"</value>
</data>
<data name="LabelVersion" xml:space="preserve">
<value>Version</value>
</data>
<data name="Loading" xml:space="preserve">
<value>Loading...</value>
</data>
<data name="PlaceholderNuGetSearch" xml:space="preserve">
<value>Search...</value>
</data>
<data name="RadioAssembly" xml:space="preserve">
<value>Load from assembly file</value>
</data>
<data name="RadioNuGet" xml:space="preserve">
<value>Load from NuGet package</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Select source</value>
</data>
</root>

View File

@ -0,0 +1,419 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using PettingZoo.Core.Settings;
using PettingZoo.Tapeti.AssemblyLoader;
using PettingZoo.Tapeti.NuGet;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.Tapeti.UI.PackageSelection
{
public enum PackageSelectionSource
{
Assembly,
NuGet
}
public class PackageSelectionViewModel : BaseViewModel
{
private readonly INuGetPackageManager nuGetPackageManager;
private readonly DelegateCommand selectCommand;
private PackageSelectionSource packageSelectionSource = PackageSelectionSource.Assembly;
private string assemblyFilename = "";
private readonly DelegateCommand assemblyBrowse;
private string nuGetSearchTerm = "";
private bool nuGetIncludePrerelease;
private INuGetPackageSource? selectedNuGetSource;
private INuGetPackage? selectedPackage;
private INuGetPackageVersion? selectedVersion;
private string packagesStatus = "";
private Visibility packagesStatusVisibility = Visibility.Collapsed;
public ICommand SelectCommand => selectCommand;
public PackageSelectionSource PackageSelectionSource
{
get => packageSelectionSource;
set => SetField(ref packageSelectionSource, value, otherPropertiesChanged: new[] { nameof(PackageSelectionSourceAssembly), nameof(PackageSelectionSourceNuGet) });
}
public bool PackageSelectionSourceAssembly
{
get => PackageSelectionSource == PackageSelectionSource.Assembly;
set
{
if (value)
PackageSelectionSource = PackageSelectionSource.Assembly;
}
}
public bool PackageSelectionSourceNuGet
{
get => PackageSelectionSource == PackageSelectionSource.NuGet;
set
{
if (value)
PackageSelectionSource = PackageSelectionSource.NuGet;
}
}
public string AssemblyFilename
{
get => assemblyFilename;
set
{
if (!SetField(ref assemblyFilename, value))
return;
if (!string.IsNullOrEmpty(value))
PackageSelectionSource = PackageSelectionSource.Assembly;
}
}
public ICommand AssemblyBrowse => assemblyBrowse;
// TODO hint for extra assemblies path
public static string HintNuGetSources => string.Format(PackageSelectionStrings.HintNuGetSources, PettingZooPaths.InstallationRoot, PettingZooPaths.AppDataRoot);
public string NuGetSearchTerm
{
get => nuGetSearchTerm;
set
{
if (!SetField(ref nuGetSearchTerm, value, otherPropertiesChanged: new[] { nameof(NuGetSearchTermPlaceholderVisibility) }))
return;
if (!string.IsNullOrEmpty(value))
PackageSelectionSource = PackageSelectionSource.NuGet;
}
}
public bool NuGetIncludePrerelease
{
get => nuGetIncludePrerelease;
set => SetField(ref nuGetIncludePrerelease, value);
}
public Visibility NuGetSearchTermPlaceholderVisibility => string.IsNullOrEmpty(NuGetSearchTerm) ? Visibility.Visible : Visibility.Hidden;
public IReadOnlyList<INuGetPackageSource> NuGetSources => nuGetPackageManager.Sources;
public INuGetPackageSource? SelectedNuGetSource
{
get => selectedNuGetSource;
set
{
if (!SetField(ref selectedNuGetSource, value))
return;
Packages.Clear();
SelectedPackage = null;
}
}
public ObservableCollectionEx<INuGetPackage> Packages { get; } = new();
public ObservableCollectionEx<INuGetPackageVersion> Versions { get; } = new();
public string PackagesStatus
{
get => packagesStatus;
set => SetField(ref packagesStatus, value);
}
public Visibility PackagesStatusVisibility
{
get => packagesStatusVisibility;
set => SetField(ref packagesStatusVisibility, value);
}
public INuGetPackage? SelectedPackage
{
get => selectedPackage;
set
{
if (!SetField(ref selectedPackage, value))
return;
Versions.Clear();
SelectedVersion = null;
}
}
public INuGetPackageVersion? SelectedVersion
{
get => selectedVersion;
set => SetField(ref selectedVersion, value);
}
public class SelectPackageEventArgs
{
public IPackageAssemblies Assemblies { get; }
public SelectPackageEventArgs(IPackageAssemblies assemblies)
{
Assemblies = assemblies;
}
}
public event EventHandler<SelectPackageEventArgs>? Select;
public PackageSelectionViewModel(INuGetPackageManager nuGetPackageManager)
{
this.nuGetPackageManager = nuGetPackageManager;
selectCommand = new DelegateCommand(SelectExecute, SelectCanExecute);
assemblyBrowse = new DelegateCommand(AssemblyBrowseExecute);
// TODO remember source
if (nuGetPackageManager.Sources.Count > 0)
selectedNuGetSource = nuGetPackageManager.Sources[^1];
var propertyChangedObservable = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
h => PropertyChanged += h,
h => PropertyChanged -= h);
propertyChangedObservable
.Where(e => e.EventArgs.PropertyName is nameof(NuGetSearchTerm) or nameof(NuGetIncludePrerelease) or nameof(SelectedNuGetSource))
.Throttle(TimeSpan.FromMilliseconds(500))
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(_ => NuGetSearch());
propertyChangedObservable
.Where(e => e.EventArgs.PropertyName == nameof(SelectedPackage))
.Throttle(TimeSpan.FromMilliseconds(100))
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(_ => NuGetGetVersions());
propertyChangedObservable
.Where(e => e.EventArgs.PropertyName is nameof(PackageSelectionSource) or nameof(AssemblyFilename) or nameof(SelectedVersion))
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(_ => selectCommand.RaiseCanExecuteChanged());
}
private void SelectExecute()
{
IPackageAssemblies? assemblies = PackageSelectionSource switch
{
PackageSelectionSource.Assembly => !string.IsNullOrWhiteSpace(AssemblyFilename) ? new FilePackageAssemblies(AssemblyFilename) : null,
PackageSelectionSource.NuGet => SelectedVersion != null ? new NuGetPackageAssemblies(SelectedVersion) : null,
_ => throw new ArgumentOutOfRangeException()
};
if (assemblies != null)
Select?.Invoke(this, new SelectPackageEventArgs(assemblies));
}
private bool SelectCanExecute()
{
return PackageSelectionSource switch
{
PackageSelectionSource.Assembly => !string.IsNullOrWhiteSpace(AssemblyFilename),
PackageSelectionSource.NuGet => SelectedVersion != null,
_ => false
};
}
private void AssemblyBrowseExecute()
{
var dialog = new OpenFileDialog
{
CheckFileExists = true,
Filter = PackageSelectionStrings.AssemblyFileFilter
};
if (!dialog.ShowDialog().GetValueOrDefault())
return;
AssemblyFilename = dialog.FileName;
}
private CancellationTokenSource? nuGetSearchCancellationTokenSource;
private void NuGetSearch()
{
if (SelectedNuGetSource == null)
return;
nuGetSearchCancellationTokenSource?.Cancel();
nuGetSearchCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = nuGetSearchCancellationTokenSource.Token;
var source = SelectedNuGetSource;
var searchTerm = NuGetSearchTerm;
var includePrerelease = NuGetIncludePrerelease;
SelectedPackage = null;
Packages.Clear();
PackagesStatus = PackageSelectionStrings.Loading;
PackagesStatusVisibility = Visibility.Visible;
Task.Run(async () =>
{
try
{
var packages = await source.Search(searchTerm, includePrerelease, cancellationToken);
await Application.Current.Dispatcher.BeginInvoke(() =>
{
Packages.ReplaceAll(packages);
SelectedPackage = null;
PackagesStatus = "";
PackagesStatusVisibility = Visibility.Collapsed;
Versions.Clear();
SelectedVersion = null;
});
}
catch (OperationCanceledException)
{
// By design...
}
catch (Exception e)
{
PackagesStatus = e.Message;
PackagesStatusVisibility = Visibility.Visible;
}
}, CancellationToken.None);
}
private CancellationTokenSource? nuGetGetVersionsCancellationTokenSource;
private void NuGetGetVersions()
{
if (SelectedPackage == null)
return;
nuGetGetVersionsCancellationTokenSource?.Cancel();
nuGetGetVersionsCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = nuGetGetVersionsCancellationTokenSource.Token;
var package = SelectedPackage;
Task.Run(async () =>
{
try
{
var versions = await package.GetVersions(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
await Application.Current.Dispatcher.BeginInvoke(() =>
{
if (cancellationToken.IsCancellationRequested)
return;
Versions.ReplaceAll(versions);
SelectedVersion = versions.Count > 0 ? versions[0] : null;
});
}
catch (OperationCanceledException)
{
// By design...
}
}, CancellationToken.None);
}
}
public class DesignTimePackageSelectionViewModel : PackageSelectionViewModel
{
public DesignTimePackageSelectionViewModel() : base(new DesignTimeNuGetPackageManager())
{
Packages.ReplaceAll(new []
{
new DesignTimeNuGetPackage("Tapeti", "Shameless plug", "M. van Renswoude", "2.8"),
new DesignTimeNuGetPackage("Messaging.Example", "Some messaging package with a very long description to test the text trimming. It should be very very very very very very very very very very very long indeed.", "Anonymoose", "0.9")
});
PackagesStatus = @"This is a very long status message, which is not unreasonable since exceptions can occur while fetching NuGet packages and they will be displayed here.";
PackagesStatusVisibility = Visibility.Visible;
}
private class DesignTimeNuGetPackageManager : INuGetPackageManager
{
public IReadOnlyList<INuGetPackageSource> Sources { get; } = new[]
{
new DesignTimeNuGetPackageSource("nuget.org")
};
}
private class DesignTimeNuGetPackageSource : INuGetPackageSource
{
public string Name { get; }
public DesignTimeNuGetPackageSource(string name)
{
Name = name;
}
public Task<IReadOnlyList<INuGetPackage>> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken)
{
return Task.FromResult(Array.Empty<INuGetPackage>() as IReadOnlyList<INuGetPackage>);
}
}
private class DesignTimeNuGetPackage : INuGetPackage
{
public string Title { get; }
public string Description { get; }
public string Authors { get; }
public string Version { get; }
public DesignTimeNuGetPackage(string title, string description, string authors, string version)
{
Title = title;
Description = description;
Authors = authors;
Version = version;
}
public Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken)
{
return Task.FromResult(Array.Empty<INuGetPackageVersion>() as IReadOnlyList<INuGetPackageVersion>);
}
}
}
}

View File

@ -0,0 +1,126 @@
<Window x:Class="PettingZoo.Tapeti.UI.PackageSelection.PackageSelectionWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:packageSelection="clr-namespace:PettingZoo.Tapeti.UI.PackageSelection"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
mc:Ignorable="d"
Title="{x:Static packageSelection:PackageSelectionStrings.WindowTitle}"
Height="600"
Width="800"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterOwner"
d:DataContext="{d:DesignInstance packageSelection:DesignTimePackageSelectionViewModel, IsDesignTimeCreatable=True}">
<Window.Resources>
<ResourceDictionary>
<Style x:Key="PackageTitle" TargetType="{x:Type TextBlock}">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<Style x:Key="PackageAuthors" TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="4,0,0,0" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<Style x:Key="PackageDescription" TargetType="{x:Type TextBlock}">
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/PettingZoo.WPF;component/Style.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<controls:GridLayout Style="{StaticResource Form}" Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<RadioButton Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Content="{x:Static packageSelection:PackageSelectionStrings.RadioAssembly}" IsChecked="{Binding PackageSelectionSourceAssembly}" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding AssemblyFilename, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1" Grid.Column="2" Content="{x:Static packageSelection:PackageSelectionStrings.ButtonBrowse}" Command="{Binding AssemblyBrowse}" />
<RadioButton Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Content="{x:Static packageSelection:PackageSelectionStrings.RadioNuGet}" IsChecked="{Binding PackageSelectionSourceNuGet}" />
<ComboBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding NuGetSources}" SelectedValue="{Binding SelectedNuGetSource}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding NuGetSearchTerm, UpdateSourceTrigger=PropertyChanged}" />
<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}" />
<ListBox Grid.Row="7" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Packages}" SelectedValue="{Binding SelectedPackage}" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Title}" Style="{StaticResource PackageTitle}" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="by" Style="{StaticResource PackageAuthors}" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Authors}" Style="{StaticResource PackageAuthors}" />
<TextBlock Grid.Row="0" Grid.Column="3" Text="{Binding Version}" />
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding Description}" Style="{StaticResource PackageDescription}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="7" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding PackagesStatus}" Visibility="{Binding PackagesStatusVisibility}" HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" TextAlignment="Center" Padding="32,0,32,0"/>
<TextBlock Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="2" Text="{x:Static packageSelection:PackageSelectionStrings.LabelVersion}" />
<ComboBox Grid.Row="9" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Versions}" SelectedValue="{Binding SelectedVersion}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Version}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding HintNuGetSources}" TextWrapping="Wrap" />
<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}" />
</StackPanel>
</controls:GridLayout>
</Window>

View File

@ -0,0 +1,14 @@
namespace PettingZoo.Tapeti.UI.PackageSelection
{
/// <summary>
/// Interaction logic for PackageSelectionWindow.xaml
/// </summary>
public partial class PackageSelectionWindow
{
public PackageSelectionWindow(PackageSelectionViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="FluentAssertions.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PettingZoo.Tapeti\PettingZoo.Tapeti.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,65 @@
using FluentAssertions;
using FluentAssertions.Execution;
using FluentAssertions.Json;
using Newtonsoft.Json.Linq;
using PettingZoo.Tapeti;
using Xunit;
#nullable disable
namespace PettingZoo.Test.Tapeti
{
public class TypeToJObjectTest
{
[Fact]
public void TestAllSupportedTypes()
{
var converted = TypeToJObjectConverter.Convert(typeof(AllSupportedTypesTest));
using (new AssertionScope())
{
// Directly supported
converted.Should().HaveElement("StringValue").Which.Should().HaveValue("");
converted.Should().HaveElement("IntValue").Which.Should().Match(t => t.Type == JTokenType.Integer);
converted.Should().HaveElement("BoolValue").Which.Should().Match(t => t.Type == JTokenType.Boolean);
var guidValue = converted.Should().HaveElement("GuidValue").Which.Should().Match(t => t.Type == JTokenType.String)
.And.Subject.Value<string>();
Guid.TryParse(guidValue, out _).Should().BeTrue();
var objectValue = converted.Should().HaveElement("ObjectValue").Subject;
objectValue.Should().HaveElement("SubStringValue").Which.Should().HaveValue("");
objectValue.Should().HaveElement("SubIntValue").Which.Should().Match(t => t.Type == JTokenType.Integer);
objectValue.Should().HaveElement("RecursiveValue").Which.Type.Should().Be(JTokenType.Null);
// Via type mapping
// TODO
}
}
// ReSharper disable UnusedMember.Local
private class AllSupportedTypesTest
{
public string StringValue { get; set; }
public int IntValue { get; set; }
public bool BoolValue { get; set; }
public Guid GuidValue { get; set; }
public ClassProperty ObjectValue { get; set; }
}
// ReSharper disable once ClassNeverInstantiated.Local
private class ClassProperty
{
public string SubStringValue { get; set; }
public int SubIntValue { get; set; }
public AllSupportedTypesTest RecursiveValue { get; set; }
}
// ReSharper restore UnusedMember.Local
}
}

View File

@ -1,7 +1,7 @@
using System.Windows;
using System.Windows.Controls;
namespace PettingZoo.UI
namespace PettingZoo.WPF.Controls
{
// Source: http://daniel-albuschat.blogspot.nl/2011/07/gridlayout-for-wpf-escape-margin-hell.html

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.1.3.50" />
</ItemGroup>
<ItemGroup>
<Reference Include="ICSharpCode.AvalonEdit">
<HintPath>C:\Users\PsychoMark\.nuget\packages\avalonedit\6.1.2.30\lib\net5.0-windows7.0\ICSharpCode.AvalonEdit.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Page Update="Style.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
</ItemGroup>
</Project>

View File

@ -1,16 +1,17 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:PettingZoo.UI">
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls">
<!-- Global styling -->
<Style x:Key="WindowStyle" TargetType="{x:Type Window}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
</Style>
<Style TargetType="{x:Type Button}">
<Style x:Key="{x:Type Button}" TargetType="{x:Type Button}">
<Setter Property="Padding" Value="8,4"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Style x:Key="{x:Type TextBox}" TargetType="{x:Type TextBox}">
<Setter Property="Padding" Value="3" />
</Style>
@ -25,7 +26,7 @@
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.InactiveCaptionBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveCaptionTextBrushKey}}"/>
</Style>
<Style x:Key="FooterPanel" TargetType="{x:Type Panel}">
<Setter Property="Margin" Value="0,8,0,0" />
</Style>
@ -39,10 +40,17 @@
</Style>
<Style x:Key="Form" TargetType="{x:Type ui:GridLayout}">
<Style x:Key="Form" TargetType="{x:Type controls:GridLayout}">
<Setter Property="ChildMargin" Value="4"/>
</Style>
<Style x:Key="Placeholder" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Padding" Value="6,0,0,0" />
<Setter Property="Foreground" Value="{x:Static SystemColors.GrayTextBrush}" />
<Setter Property="IsHitTestVisible" Value="False" />
</Style>
<Style x:Key="Properties" TargetType="{x:Type DataGrid}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
@ -83,10 +91,11 @@
</Style>
<Style x:Key="Payload" TargetType="{x:Type TextBox}">
<Setter Property="AcceptsReturn" Value="True" />
<Setter Property="AcceptsTab" Value="True" />
<Setter Property="VerticalScrollBarVisibility" Value="Visible" />
<Style x:Key="Payload" TargetType="{x:Type avalonedit:TextEditor}">
<Setter Property="FontFamily" Value="Consolas,Courier New" />
</Style>
<Style x:Key="ControlBorder" TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="1" />
</Style>
</ResourceDictionary>

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PettingZoo.UI
namespace PettingZoo.WPF.ViewModel
{
public class BaseViewModel : INotifyPropertyChanged
{

View File

@ -0,0 +1,40 @@
using System;
using System.Windows.Input;
namespace PettingZoo.WPF.ViewModel
{
public class DelegateCommand : ICommand
{
private readonly Func<bool>? canExecute;
private readonly Action execute;
public event EventHandler? CanExecuteChanged;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool>? canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return canExecute == null || canExecute();
}
public void Execute(object? parameter)
{
execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
// ReSharper disable UnusedMember.Global - public API
namespace PettingZoo.WPF.ViewModel
{
public class ObservableCollectionEx<T> : ObservableCollection<T>
{
private int updateLockCount;
private bool updatedInLock;
public void AddRange(IEnumerable<T> items)
{
BeginUpdate();
try
{
foreach (var item in items)
Add(item);
}
finally
{
EndUpdate();
}
}
public void ReplaceAll(IEnumerable<T> newItems)
{
BeginUpdate();
try
{
Clear();
foreach (var item in newItems)
Add(item);
}
finally
{
EndUpdate();
}
}
/// <summary>
/// Disables the OnCollectionChanged notification. Should always be matched with an EndUpdate to resume notifications.
/// </summary>
public void BeginUpdate()
{
updateLockCount++;
}
public void EndUpdate()
{
if (updateLockCount == 0)
throw new InvalidOperationException("EndUpdate does not have a matching BeginUpdate");
updateLockCount--;
if (updateLockCount == 0 && updatedInLock)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (updateLockCount > 0)
{
updatedInLock = true;
return;
}
base.OnCollectionChanged(e);
}
}
}

View File

@ -16,7 +16,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "Pett
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Tapeti", "PettingZoo.Tapeti\PettingZoo.Tapeti.csproj", "{1763AB04-59D9-4663-B207-D6302FFAACD5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.WPF", "PettingZoo.WPF\PettingZoo.WPF.csproj", "{E6617B69-2AC4-4056-B801-DD32E2374B71}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Test", "PettingZoo.Test\PettingZoo.Test.csproj", "{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -44,6 +48,14 @@ Global
{7157B09C-FDD9-4928-B14D-C25B784CA865}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.Build.0 = Release|Any CPU
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Release|Any CPU.Build.0 = Release|Any CPU
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

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

View File

@ -6,7 +6,7 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Style.xaml"/>
<ResourceDictionary Source="pack://application:,,,/PettingZoo.WPF;component/Style.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

View File

@ -6,6 +6,7 @@ using System.Windows;
using System.Windows.Threading;
using PettingZoo.Core.Settings;
using PettingZoo.UI.Main;
using Serilog;
using SimpleInjector;
using Point = System.Windows.Point;
@ -14,6 +15,7 @@ namespace PettingZoo
public partial class App
{
private readonly Container container;
private readonly ILogger logger;
public App()
@ -22,9 +24,10 @@ namespace PettingZoo
}
public App(Container container)
public App(Container container, ILogger logger)
{
this.container = container;
this.logger = logger;
}
@ -80,6 +83,7 @@ namespace PettingZoo
private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
logger.Error(e.Exception, "Unhandled exception");
_ = MessageBox.Show($"Unhandled exception: {e.Exception.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
}
}

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#EFEBDE;" points="46.5,14 32.5,0 1.5,0 1.5,58 46.5,58 "/>
<g>
<path style="fill:#D5D0BB;" d="M11.5,23h25c0.552,0,1-0.447,1-1s-0.448-1-1-1h-25c-0.552,0-1,0.447-1,1S10.948,23,11.5,23z"/>
<path style="fill:#D5D0BB;" d="M11.5,15h10c0.552,0,1-0.447,1-1s-0.448-1-1-1h-10c-0.552,0-1,0.447-1,1S10.948,15,11.5,15z"/>
<path style="fill:#D5D0BB;" d="M36.5,29h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S37.052,29,36.5,29z"/>
<path style="fill:#D5D0BB;" d="M36.5,37h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S37.052,37,36.5,37z"/>
<path style="fill:#D5D0BB;" d="M36.5,45h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S37.052,45,36.5,45z"/>
</g>
<polygon style="fill:#D5D0BB;" points="32.5,0 32.5,14 46.5,14 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 59.39 59.39" style="enable-background:new 0 0 59.39 59.39;" xml:space="preserve">
<g>
<polygon style="fill:#556080;" points="25,10.695 20,3.695 0,3.695 0,10.695 0,54.695 58,54.695 58,17.695 30,17.695 "/>
<polygon style="fill:#3D4451;" points="30,17.695 58,17.695 58,10.695 25,10.695 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -19,9 +19,11 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Images\Connecting.svg" />
<None Remove="Images\Busy.svg" />
<None Remove="Images\Dock.svg" />
<None Remove="Images\Error.svg" />
<None Remove="Images\Example.svg" />
<None Remove="Images\Folder.svg" />
<None Remove="Images\Ok.svg" />
<None Remove="Images\PublishSend.svg" />
<None Remove="Images\Undock.svg" />
@ -33,6 +35,8 @@
<Resource Include="Images\Disconnect.svg" />
<Resource Include="Images\Dock.svg" />
<Resource Include="Images\Error.svg" />
<Resource Include="Images\Example.svg" />
<Resource Include="Images\Folder.svg" />
<Resource Include="Images\Ok.svg" />
<Resource Include="Images\Publish.svg" />
<Resource Include="Images\PublishSend.svg" />
@ -40,7 +44,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.1.3.50" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SharpVectors" Version="1.7.7" />
<PackageReference Include="SimpleInjector" Version="5.3.2" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
@ -50,11 +57,13 @@
<ProjectReference Include="..\PettingZoo.Core\PettingZoo.Core.csproj" />
<ProjectReference Include="..\PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj" />
<ProjectReference Include="..\PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj" />
<ProjectReference Include="..\PettingZoo.Tapeti\PettingZoo.Tapeti.csproj" />
<ProjectReference Include="..\PettingZoo.WPF\PettingZoo.WPF.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="Images\Undock.svg" />
<Resource Include="Images\Connecting.svg" />
<Resource Include="Images\Busy.svg" />
</ItemGroup>
<ItemGroup>
@ -68,11 +77,6 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Example\ExamplePickerDialogStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ExamplePickerDialogStrings.resx</DependentUpon>
</Compile>
<Compile Update="UI\Main\MainWindowStrings.Designer.cs">
<DependentUpon>MainWindowStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
@ -127,10 +131,6 @@
<LastGenOutput>ConnectionWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Example\ExamplePickerDialogStrings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>ExamplePickerDialogStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="UI\Main\MainWindowStrings.resx">
<LastGenOutput>MainWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>

View File

@ -1,15 +1,18 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using System.IO;
using System.Windows;
using System.Windows.Markup;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.Core.Settings;
using PettingZoo.RabbitMQ;
using PettingZoo.Settings.LiteDB;
using PettingZoo.Tapeti;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Main;
using PettingZoo.UI.Subscribe;
using Serilog;
using SimpleInjector;
namespace PettingZoo
@ -23,23 +26,59 @@ namespace PettingZoo
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(
XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
var container = Bootstrap();
RunApplication(container);
try
{
var logger = CreateLogger();
try
{
logger.Verbose("Bootstrapping...");
var container = Bootstrap(logger);
logger.Verbose("Running application...");
RunApplication(container, logger);
}
catch (Exception e)
{
logger.Error(e, "Unhandled exception");
throw;
}
finally
{
logger.Verbose("Shutting down");
}
}
catch (Exception e)
{
_ = MessageBox.Show($"Unhandled exception: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static Container Bootstrap()
private static ILogger CreateLogger()
{
var logPath = Path.Combine(PettingZooPaths.LogPath, @"PettingZoo.log");
return new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.File(logPath, rollingInterval: RollingInterval.Day)
.CreateLogger();
}
private static Container Bootstrap(ILogger logger)
{
var container = new Container();
// See comments in RunApplication
container.Options.EnableAutoVerification = false;
container.RegisterInstance(logger);
container.Register<IConnectionFactory, RabbitMQClientConnectionFactory>();
container.Register<IConnectionDialog, WindowConnectionDialog>();
container.Register<ISubscribeDialog, WindowSubscribeDialog>();
container.Register<IConnectionSettingsRepository, LiteDBConnectionSettingsRepository>();
container.Register<IUISettingsRepository, LiteDBUISettingsRepository>();
container.Register<IExampleGenerator, TapetiClassLibraryExampleGenerator>();
container.Register<MainWindow>();
@ -47,32 +86,25 @@ namespace PettingZoo
}
private static void RunApplication(Container container)
private static void RunApplication(Container container, ILogger logger)
{
try
{
var app = new App(container);
app.InitializeComponent();
var app = new App(container, logger);
app.InitializeComponent();
#if DEBUG
// Verify container after initialization to prevent issues loading the resource dictionaries
container.Verify();
#if DEBUG
// Verify container after initialization to prevent issues loading the resource dictionaries
container.Verify();
// This causes the MainWindow and Windows properties to be populated however, which we don't want
// because then the app does not close properly when using OnMainWindowClose, so clean up the mess
app.MainWindow = null;
foreach (var window in app.Windows)
((Window)window).Close();
// All this is the reason we only perform verification in debug builds
#endif
app.Run();
}
catch (Exception e)
{
MessageBox.Show($"Fatal exception: {e.Message}", @"PettingZoo", MessageBoxButton.OK, MessageBoxImage.Error);
}
// This causes the MainWindow and Windows properties to be populated however, which we don't want
// because then the app does not close properly when using OnMainWindowClose, so clean up the mess
app.MainWindow = null;
foreach (var window in app.Windows)
((Window)window).Close();
// All this is the reason we only perform verification in debug builds
#endif
app.Run();
}
}
}

View File

@ -1,13 +1,15 @@
Must-have
---------
- Check required fields before enabling Publish button
Should-have
-----------
- Save / load publisher messages (either as templates or to disk)
- Export received messages to Tapeti JSON file / Tapeti.Cmd command-line
- Tapeti: export received messages to Tapeti.Cmd JSON file / Tapeti.Cmd command-line
- Tapeti: fetch NuGet dependencies to improve the chances of succesfully loading the assembly, instead of the current "extraAssembliesPaths" workaround
Nice-to-have
------------
- JSON syntax highlighting
- Validation against message classes (for Tapeti messages)

View File

@ -1,5 +1,6 @@
using System;
using System.Windows.Input;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Connection
{

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using PettingZoo.Core.Settings;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Connection
{
@ -207,10 +208,7 @@ namespace PettingZoo.UI.Connection
Port > 0 &&
!string.IsNullOrWhiteSpace(Username) &&
(!requirePassword || !string.IsNullOrWhiteSpace(Password)) &&
(!Subscribe || (
!string.IsNullOrWhiteSpace(Exchange) &&
!string.IsNullOrWhiteSpace(RoutingKey)
));
(!Subscribe || !string.IsNullOrWhiteSpace(Exchange) && !string.IsNullOrWhiteSpace(RoutingKey));
}

View File

@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:connection="clr-namespace:PettingZoo.UI.Connection"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance connection:DesignTimeConnectionViewModel, IsDesignTimeCreatable = True}"
Width="700"
@ -58,7 +59,7 @@
</ListBox.ItemTemplate>
</ListBox>
<ui:GridLayout Style="{StaticResource Form}" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2">
<controls:GridLayout Style="{StaticResource Form}" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@ -98,7 +99,7 @@
<Label Grid.Column="0" Grid.Row="9" Content="{x:Static connection:ConnectionWindowStrings.LabelRoutingKey}"/>
<TextBox Grid.Column="1" Grid.Row="9" Text="{Binding RoutingKey, UpdateSourceTrigger=PropertyChanged}" GotFocus="CaretToEnd"/>
</ui:GridLayout>
</controls:GridLayout>
</Grid>
</Window>

View File

@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using PettingZoo.Core.Settings;
namespace PettingZoo.UI.Connection

View File

@ -1,79 +0,0 @@
using System;
using System.Windows.Input;
namespace PettingZoo.UI
{
public class DelegateCommand<T> : ICommand where T : class?
{
private readonly Func<T?, bool>? canExecute;
private readonly Action<T?> execute;
public event EventHandler? CanExecuteChanged;
public DelegateCommand(Action<T?> execute) : this(execute, null)
{
}
public DelegateCommand(Action<T?> execute, Func<T?, bool>? canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return canExecute == null || canExecute((T?)parameter);
}
public void Execute(object? parameter)
{
execute((T?)parameter);
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
public class DelegateCommand : ICommand
{
private readonly Func<bool>? canExecute;
private readonly Action execute;
public event EventHandler? CanExecuteChanged;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool>? canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return canExecute == null || canExecute();
}
public void Execute(object? parameter)
{
execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@ -13,7 +13,7 @@ namespace PettingZoo.UI
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((bool)value) ? parameter : Binding.DoNothing;
return (bool)value ? parameter : Binding.DoNothing;
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
namespace PettingZoo.UI
{
public class ErrorHighlightingTransformer : DocumentColorizingTransformer
{
public Brush BackgroundBrush { get; set; }
public TextPosition? ErrorPosition { get; set; }
public ErrorHighlightingTransformer()
{
BackgroundBrush = new SolidColorBrush(Color.FromRgb(255, 230, 230));
}
protected override void ColorizeLine(DocumentLine line)
{
if (ErrorPosition == null)
return;
if (line.LineNumber != Math.Max(ErrorPosition.Value.Row, 1))
return;
var lineStartOffset = line.Offset;
ChangeLinePart(lineStartOffset, lineStartOffset + line.Length,
element =>
{
element.BackgroundBrush = BackgroundBrush;
});
}
}
}

View File

@ -1,12 +0,0 @@
<Window x:Class="PettingZoo.UI.Example.ExamplePickerDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:example="clr-namespace:PettingZoo.UI.Example"
mc:Ignorable="d"
Title="{x:Static example:ExamplePickerDialogStrings.WindowTitle}" Height="800" Width="600">
<Grid>
</Grid>
</Window>

View File

@ -1,15 +0,0 @@
using System.Windows;
namespace PettingZoo.UI.Example
{
/// <summary>
/// Interaction logic for ExamplePickerDialog.xaml
/// </summary>
public partial class ExamplePickerDialog : Window
{
public ExamplePickerDialog()
{
InitializeComponent();
}
}
}

View File

@ -1,6 +0,0 @@
namespace PettingZoo.UI.Example
{
public class ExamplePickerDialogViewModel
{
}
}

View File

@ -6,6 +6,8 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
// ReSharper disable UnusedMember.Global - public API
namespace PettingZoo.UI
{
// Source: https://social.msdn.microsoft.com/Forums/vstudio/en-US/0f524459-b14e-4f9a-8264-267953418a2d/trivial-listboxlistview-autoscroll?forum=wpf
@ -69,6 +71,8 @@ namespace PettingZoo.UI
public void Dispose()
{
BindingOperations.ClearBinding(this, ItemsSourceProperty);
GC.SuppressFinalize(this);
}

View File

@ -79,7 +79,7 @@
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Connecting.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusConnecting}" />
<Image Source="{svgc:SvgImage Source=/Images/Busy.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusConnecting}" />
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusOk}" />
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusError}" />
<TextBlock Text="{Binding ConnectionStatus}" VerticalAlignment="Center"/>

View File

@ -5,6 +5,7 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
@ -19,11 +20,15 @@ namespace PettingZoo.UI.Main
public bool WasMaximized;
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog)
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, IExampleGenerator exampleGenerator)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen;
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this);
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this, exampleGenerator)
{
TabHostWindow = this
};
DataContext = viewModel;
InitializeComponent();
@ -115,6 +120,8 @@ namespace PettingZoo.UI.Main
private static T? GetParent<T>(object originalSource) where T : DependencyObject
{
var current = originalSource as DependencyObject;
if (current is not Visual)
return null;
while (current != null)
{

View File

@ -6,10 +6,12 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
using PettingZoo.UI.Tab.Undocked;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Main
{
@ -44,6 +46,8 @@ namespace PettingZoo.UI.Main
private ConnectionStatusType connectionStatusType;
public Window? TabHostWindow { get; set; }
public string ConnectionStatus
{
@ -75,8 +79,8 @@ namespace PettingZoo.UI.Main
if (!SetField(ref activeTab, value, otherPropertiesChanged: new[] { nameof(ToolbarCommands), nameof(ToolbarCommandsSeparatorVisibility) }))
return;
currentTab?.Deactivate();
activeTab?.Activate();
(currentTab as ITabActivate)?.Deactivate();
(activeTab as ITabActivate)?.Activate();
}
}
@ -99,7 +103,7 @@ namespace PettingZoo.UI.Main
public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog,
ISubscribeDialog subscribeDialog, ITabContainer tabContainer)
ISubscribeDialog subscribeDialog, ITabContainer tabContainer, IExampleGenerator exampleGenerator)
{
this.connectionFactory = connectionFactory;
this.connectionDialog = connectionDialog;
@ -117,7 +121,7 @@ namespace PettingZoo.UI.Main
closeTabCommand = new DelegateCommand(CloseTabExecute, HasActiveTabCanExecute);
undockTabCommand = new DelegateCommand(UndockTabExecute, HasActiveTabCanExecute);
tabFactory = new ViewTabFactory(this);
tabFactory = new ViewTabFactory(this, exampleGenerator);
}
@ -226,6 +230,7 @@ namespace PettingZoo.UI.Main
undockedTabs.Add(tab, tabHostWindow);
tabHostWindow.Show();
(tab as ITabHostWindowNotify)?.HostWindowChanged(tabHostWindow);
}
@ -263,7 +268,9 @@ namespace PettingZoo.UI.Main
{
Tabs.Add(tab);
ActiveTab = tab;
(tab as ITabHostWindowNotify)?.HostWindowChanged(TabHostWindow);
closeTabCommand.RaiseCanExecuteChanged();
undockTabCommand.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(NoTabsVisibility));
@ -276,6 +283,9 @@ namespace PettingZoo.UI.Main
tabHostWindow.Close();
AddTab(tab);
ActiveTab = tab;
(tab as ITabHostWindowNotify)?.HostWindowChanged(TabHostWindow);
}
public void UndockedTabClosed(ITab tab)
@ -320,7 +330,7 @@ namespace PettingZoo.UI.Main
public class DesignTimeMainWindowViewModel : MainWindowViewModel
{
public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!)
public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!, null!)
{
}
}

View File

@ -1,6 +1,8 @@
using System.Windows;
using System.Windows.Controls;
// ReSharper disable UnusedMember.Global - public API
namespace PettingZoo.UI
{
// Source: http://blog.functionalfun.net/2008/06/wpf-passwordbox-and-data-binding.html
@ -20,14 +22,10 @@ namespace PettingZoo.UI
private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var box = d as PasswordBox;
// only handle this event when the property is attached to a PasswordBox
// and when the BindPassword attached property has been set to true
if (box == null || !GetBindPassword(d))
{
if (d is not PasswordBox box || !GetBindPassword(d))
return;
}
// avoid recursive updating by ignoring the box's changed event
box.PasswordChanged -= HandlePasswordChanged;
@ -61,8 +59,7 @@ namespace PettingZoo.UI
private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
{
var box = sender as PasswordBox;
if (box == null)
if (sender is not PasswordBox box)
return;
// set a flag to indicate that we're updating the password

View File

@ -1,6 +1,4 @@
using System;
namespace PettingZoo.UI.Subscribe
namespace PettingZoo.UI.Subscribe
{
public interface ISubscribeDialog
{

View File

@ -1,5 +1,6 @@
using System;
using System.Windows.Input;
using PettingZoo.WPF.ViewModel;
// TODO validate input
@ -40,7 +41,7 @@ namespace PettingZoo.UI.Subscribe
public SubscribeDialogParams ToModel()
{
return new(Exchange, RoutingKey);
return new SubscribeDialogParams(Exchange, RoutingKey);
}

View File

@ -3,11 +3,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:connection="clr-namespace:PettingZoo.UI.Connection"
xmlns:subscribe="clr-namespace:PettingZoo.UI.Subscribe"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance connection:DesignTimeConnectionViewModel, IsDesignTimeCreatable = True}"
d:DataContext="{d:DesignInstance subscribe:DesignTimeSubscribeViewModel, IsDesignTimeCreatable=True}"
Width="500"
SizeToContent="Height"
ResizeMode="NoResize"
@ -21,7 +20,7 @@
<Button IsCancel="True" Content="{x:Static subscribe:SubscribeWindowStrings.ButtonCancel}" Style="{StaticResource FooterButton}"/>
</UniformGrid>
<ui:GridLayout Style="{StaticResource Form}">
<controls:GridLayout Style="{StaticResource Form}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@ -35,7 +34,7 @@
<Label Grid.Column="0" Grid.Row="1" Content="{x:Static subscribe:SubscribeWindowStrings.LabelRoutingKey}"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding RoutingKey}"/>
</ui:GridLayout>
</controls:GridLayout>
</DockPanel>
</Window>

View File

@ -1,12 +1,20 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace PettingZoo.UI.Tab
{
public interface ITabToolbarCommands : INotifyPropertyChanged
public interface ITab : INotifyPropertyChanged
{
string Title { get; }
ContentControl Content { get; }
}
public interface ITabToolbarCommands
{
IEnumerable<TabToolbarCommand> ToolbarCommands { get; }
}
@ -19,13 +27,12 @@ namespace PettingZoo.UI.Tab
}
public interface ITab : ITabToolbarCommands, ITabActivate
public interface ITabHostWindowNotify
{
string Title { get; }
ContentControl Content { get; }
void HostWindowChanged(Window? hostWindow);
}
public readonly struct TabToolbarCommand
{
public ICommand Command { get; }

View File

@ -17,15 +17,23 @@
<RadioButton Content="JSON" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Json}}" />
<RadioButton Content="Plain text" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Plain}}" />
<RadioButton Content="Other" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Other}}" />
<TextBox Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Name="TextBoxForBorder" Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding JsonValidationVisibility}" Margin="0,8,0,0">
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationOk}" />
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationError}" />
<TextBlock Text="{Binding JsonValidationMessage}" Margin="4" />
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding ValidationVisibility}" Margin="0,8,0,0">
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationOk}" />
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationError}" />
<Image Source="{svgc:SvgImage Source=/Images/Busy.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationValidating}" />
<TextBlock Text="{Binding ValidationMessage}" Margin="4" />
</StackPanel>
<TextBox Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource Payload}" />
<Border Style="{StaticResource ControlBorder}" Name="EditorBorder">
<avalonedit:TextEditor
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
Name="Editor"
SyntaxHighlighting="{Binding SyntaxHighlighting}"
Style="{StaticResource Payload}"
/>
</Border>
</DockPanel>
</UserControl>

View File

@ -86,6 +86,9 @@ namespace PettingZoo.UI.Tab.Publisher
}
}
private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new();
public PayloadEditorControl()
{
// Keep the exposed properties in sync with the ViewModel
@ -133,6 +136,63 @@ namespace PettingZoo.UI.Tab.Publisher
InitializeComponent();
// I'm not sure how to get a standard control border, all I could find were workaround:
// https://social.msdn.microsoft.com/Forums/en-US/5e007497-8d5a-401d-ac5b-9e1356fe9b64/default-borderbrush-for-textbox-listbox-etc
// So I'll just copy it from another TextBox. I truly hate WPF some times for making standard things so complicated. </rant>
EditorBorder.BorderBrush = TextBoxForBorder.BorderBrush;
Editor.Options.IndentationSize = 2;
Editor.TextArea.TextView.LineTransformers.Add(errorHighlightingTransformer);
// Avalon doesn't play nice with bindings it seems:
// https://stackoverflow.com/questions/18964176/two-way-binding-to-avalonedit-document-text-using-mvvm
Editor.Document.Text = Payload;
var editorTriggered = false;
Editor.TextChanged += (_, _) =>
{
editorTriggered = true;
try
{
Payload = Editor.Document.Text;
}
finally
{
editorTriggered = false;
}
};
viewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName != nameof(viewModel.ValidationInfo) &&
(args.PropertyName != nameof(viewModel.Payload) || editorTriggered))
return;
Dispatcher.Invoke(() =>
{
switch (args.PropertyName)
{
case nameof(viewModel.ValidationInfo):
if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
return;
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
// TODO this can probably be optimized to only redraw the affected line
Editor.TextArea.TextView.Redraw();
break;
case nameof(viewModel.Payload):
Editor.Document.Text = viewModel.Payload;
break;
}
});
};
// Setting the DataContext for the UserControl is a major PITA when binding the control's properties,
// so I've moved the ViewModel one level down to get the best of both worlds...
DataContextContainer.DataContext = viewModel;

View File

@ -88,20 +88,29 @@ namespace PettingZoo.UI.Tab.Publisher {
}
/// <summary>
/// Looks up a localized string similar to Invalid JSON: {0}.
/// Looks up a localized string similar to Invalid: {0}.
/// </summary>
internal static string JsonValidationError {
internal static string ValidationError {
get {
return ResourceManager.GetString("JsonValidationError", resourceCulture);
return ResourceManager.GetString("ValidationError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Valid JSON.
/// Looks up a localized string similar to Valid.
/// </summary>
internal static string JsonValidationOk {
internal static string ValidationOk {
get {
return ResourceManager.GetString("JsonValidationOk", resourceCulture);
return ResourceManager.GetString("ValidationOk", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Validating....
/// </summary>
internal static string ValidationValidating {
get {
return ResourceManager.GetString("ValidationValidating", 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="ContentTypeJson" xml:space="preserve">
<value>JSON</value>
@ -126,10 +126,13 @@
<data name="ContentTypePlain" xml:space="preserve">
<value>Plain text</value>
</data>
<data name="JsonValidationError" xml:space="preserve">
<value>Invalid JSON: {0}</value>
<data name="ValidationError" xml:space="preserve">
<value>Invalid: {0}</value>
</data>
<data name="JsonValidationOk" xml:space="preserve">
<value>Valid JSON</value>
<data name="ValidationOk" xml:space="preserve">
<value>Valid</value>
</data>
<data name="ValidationValidating" xml:space="preserve">
<value>Validating...</value>
</data>
</root>

View File

@ -1,9 +1,11 @@
using System;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Windows;
using ICSharpCode.AvalonEdit.Highlighting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Tab.Publisher
{
@ -12,7 +14,39 @@ namespace PettingZoo.UI.Tab.Publisher
Json,
Plain,
Other
};
}
public enum ValidationStatus
{
NotSupported,
Validating,
Ok,
Error
}
public readonly struct ValidationInfo
{
public ValidationStatus Status { get; }
public string Message { get; }
public TextPosition? ErrorPosition { get; }
public ValidationInfo(ValidationStatus status, string? message = null, TextPosition? errorPosition = null)
{
Status = status;
Message = message ?? status switch
{
ValidationStatus.NotSupported => "",
ValidationStatus.Validating => PayloadEditorStrings.ValidationValidating,
ValidationStatus.Ok => PayloadEditorStrings.ValidationOk,
ValidationStatus.Error => throw new InvalidOperationException(@"Message required for Error validation status"),
_ => throw new ArgumentException(@"Unsupported validation status", nameof(status))
};
ErrorPosition = errorPosition;
}
}
public class PayloadEditorViewModel : BaseViewModel
@ -24,8 +58,7 @@ namespace PettingZoo.UI.Tab.Publisher
private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json;
private bool fixedJson;
private bool jsonValid = true;
private string jsonValidationMessage;
private ValidationInfo validationInfo = new(ValidationStatus.Ok);
private string payload = "";
@ -59,7 +92,7 @@ namespace PettingZoo.UI.Tab.Publisher
get => contentTypeSelection;
set
{
if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(JsonValidationVisibility) }))
if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(ValidationVisibility), nameof(SyntaxHighlighting) }))
return;
ContentType = ContentTypeSelection switch
@ -80,23 +113,22 @@ namespace PettingZoo.UI.Tab.Publisher
set => SetField(ref fixedJson, value);
}
public Visibility JsonValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
public Visibility JsonValidationOk => JsonValid ? Visibility.Visible : Visibility.Collapsed;
public Visibility JsonValidationError => !JsonValid ? Visibility.Visible : Visibility.Collapsed;
public string JsonValidationMessage
public ValidationInfo ValidationInfo
{
get => jsonValidationMessage;
private set => SetField(ref jsonValidationMessage, value);
get => validationInfo;
private set => SetField(ref validationInfo, value, otherPropertiesChanged: new[] { nameof(ValidationOk), nameof(ValidationError), nameof(ValidationValidating), nameof(ValidationMessage) });
}
public bool JsonValid
{
get => jsonValid;
private set => SetField(ref jsonValid, value, otherPropertiesChanged: new[] { nameof(JsonValidationOk), nameof(JsonValidationError) });
}
public Visibility ValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
public string ValidationMessage => ValidationInfo.Message;
public Visibility ValidationOk => ValidationInfo.Status == ValidationStatus.Ok ? 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;
public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible;
@ -108,26 +140,48 @@ namespace PettingZoo.UI.Tab.Publisher
}
public IHighlightingDefinition? SyntaxHighlighting => ContentTypeSelection == PayloadEditorContentType.Json
? HighlightingManager.Instance.GetDefinition(@"Json")
: null;
public PayloadEditorViewModel()
{
jsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
var observable = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
h => PropertyChanged += h,
h => PropertyChanged -= h)
.Where(e => e.EventArgs.PropertyName == nameof(Payload))
.Where(e => e.EventArgs.PropertyName == nameof(Payload));
observable
.Subscribe(_ => ValidatingPayload());
observable
.Throttle(TimeSpan.FromMilliseconds(500))
.Subscribe(_ => ValidatePayload());
}
private void ValidatingPayload()
{
if (ValidationInfo.Status == ValidationStatus.Validating)
return;
if (ContentTypeSelection != PayloadEditorContentType.Json)
{
ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
return;
}
ValidationInfo = new ValidationInfo(ValidationStatus.Validating);
}
private void ValidatePayload()
{
if (ContentTypeSelection != PayloadEditorContentType.Json)
{
JsonValid = true;
JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
return;
}
@ -136,13 +190,15 @@ namespace PettingZoo.UI.Tab.Publisher
if (!string.IsNullOrEmpty(Payload))
JToken.Parse(Payload);
JsonValid = true;
JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
ValidationInfo = new ValidationInfo(ValidationStatus.Ok);
}
catch (JsonReaderException e)
{
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message, new TextPosition(e.LineNumber, e.LinePosition));
}
catch (Exception e)
{
JsonValid = false;
JsonValidationMessage = string.Format(PayloadEditorStrings.JsonValidationError, e.Message);
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message);
}
}
}

View File

@ -4,14 +4,14 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:res="clr-namespace:PettingZoo.UI.Tab.Publisher"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ui:GridLayout Style="{StaticResource Form}" Margin="4" Grid.IsSharedSizeScope="True">
<controls:GridLayout Style="{StaticResource Form}" Margin="4" Grid.IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@ -61,6 +61,6 @@
<ContentControl Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 8 0 0" Content="{Binding MessageTypeControl}" />
<Button Grid.Row="10" Grid.Column="1" Command="{Binding PublishCommand}" Content="{x:Static res:PublisherViewStrings.CommandPublish}" HorizontalAlignment="Left" />
</ui:GridLayout>
</controls:GridLayout>
</ScrollViewer>
</UserControl>

View File

@ -4,6 +4,8 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Tab.Publisher
{
@ -14,9 +16,10 @@ namespace PettingZoo.UI.Tab.Publisher
}
public class PublisherViewModel : BaseViewModel, ITabToolbarCommands, IPublishDestination
public class PublisherViewModel : BaseViewModel, ITabToolbarCommands, ITabHostWindowNotify, IPublishDestination
{
private readonly IConnection connection;
private readonly IExampleGenerator exampleGenerator;
private readonly ITabFactory tabFactory;
private readonly ITabHost tabHost;
@ -36,6 +39,7 @@ namespace PettingZoo.UI.Tab.Publisher
private readonly DelegateCommand publishCommand;
private readonly TabToolbarCommand[] toolbarCommands;
private Window? tabHostWindow;
public bool SendToExchange
@ -150,9 +154,10 @@ namespace PettingZoo.UI.Tab.Publisher
string IPublishDestination.RoutingKey => SendToExchange ? RoutingKey : Queue;
public PublisherViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
public PublisherViewModel(ITabHost tabHost, ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null)
{
this.connection = connection;
this.exampleGenerator = exampleGenerator;
this.tabFactory = tabFactory;
this.tabHost = tabHost;
@ -187,16 +192,35 @@ namespace PettingZoo.UI.Tab.Publisher
switch (value)
{
case MessageType.Raw:
var rawPublisherViewModel = new RawPublisherViewModel(connection, this);
rawPublisherView ??= new RawPublisherView(rawPublisherViewModel);
RawPublisherViewModel rawPublisherViewModel;
if (rawPublisherView == null)
{
rawPublisherViewModel = new RawPublisherViewModel(connection, this);
rawPublisherView ??= new RawPublisherView(rawPublisherViewModel);
}
else
rawPublisherViewModel = (RawPublisherViewModel)rawPublisherView.DataContext;
MessageTypeControl = rawPublisherView;
messageTypePublishCommand = rawPublisherViewModel.PublishCommand;
break;
case MessageType.Tapeti:
var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this);
tapetiPublisherView ??= new TapetiPublisherView(tapetiPublisherViewModel);
TapetiPublisherViewModel tapetiPublisherViewModel;
if (tapetiPublisherView == null)
{
tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator);
tapetiPublisherView ??= new TapetiPublisherView(tapetiPublisherViewModel);
if (tabHostWindow != null)
tapetiPublisherViewModel.HostWindowChanged(tabHostWindow);
}
else
tapetiPublisherViewModel = (TapetiPublisherViewModel)tapetiPublisherView.DataContext;
MessageTypeControl = tapetiPublisherView;
messageTypePublishCommand = tapetiPublisherViewModel.PublishCommand;
@ -218,7 +242,7 @@ namespace PettingZoo.UI.Tab.Publisher
if (TapetiPublisherViewModel.IsTapetiMessage(fromReceivedMessage))
{
var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, fromReceivedMessage);
var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator, fromReceivedMessage);
tapetiPublisherView = new TapetiPublisherView(tapetiPublisherViewModel);
MessageType = MessageType.Tapeti;
@ -245,12 +269,20 @@ namespace PettingZoo.UI.Tab.Publisher
subscriber.Start();
return subscriber.QueueName;
}
public void HostWindowChanged(Window? hostWindow)
{
tabHostWindow = hostWindow;
(tapetiPublisherView?.DataContext as TapetiPublisherViewModel)?.HostWindowChanged(hostWindow);
}
}
public class DesignTimePublisherViewModel : PublisherViewModel
{
public DesignTimePublisherViewModel() : base(null!, null!, null!)
public DesignTimePublisherViewModel() : base(null!, null!, null!, null!)
{
}

View File

@ -4,18 +4,18 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:publisher="clr-namespace:PettingZoo.UI.Tab.Publisher"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="800"
d:DataContext="{d:DesignInstance publisher:DesignTimeRawPublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<ui:GridLayout Style="{StaticResource Form}">
<controls:GridLayout Style="{StaticResource Form}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:GridLayout.RowDefinitions>
<controls:GridLayout.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -32,11 +32,11 @@
<RowDefinition Height="16"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</ui:GridLayout.RowDefinitions>
</controls:GridLayout.RowDefinitions>
<Label Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelDeliveryMode}" />
<ComboBox Grid.Column="1" SelectedIndex="{Binding DeliveryModeIndex}">
<Label Grid.Row="0" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelDeliveryMode}" />
<ComboBox Grid.Row="0" Grid.Column="1" SelectedIndex="{Binding DeliveryModeIndex}">
<ComboBoxItem Content="{x:Static publisher:RawPublisherViewStrings.DeliveryModeNonPersistent}" />
<ComboBoxItem Content="{x:Static publisher:RawPublisherViewStrings.DeliveryModePersistent}" />
</ComboBox>
@ -132,5 +132,5 @@
<Label Grid.Row="14" Grid.Column="0" Content="{x:Static publisher:RawPublisherViewStrings.LabelPayload}" />
<publisher:PayloadEditorControl Grid.Row="14" Grid.Column="1" Payload="{Binding Payload}" ContentType="{Binding ContentType}" Height="350"/>
</ui:GridLayout>
</controls:GridLayout>
</UserControl>

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;

View File

@ -7,6 +7,7 @@ using System.Text;
using System.Windows;
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Tab.Publisher
{
@ -144,7 +145,7 @@ namespace PettingZoo.UI.Tab.Publisher
protected Header LastHeader;
public RawPublisherViewModel(IConnection connection, IPublishDestination publishDestination, ReceivedMessageInfo? receivedMessage = null)
public RawPublisherViewModel(IConnection connection, IPublishDestination publishDestination, BaseMessageInfo? receivedMessage = null)
{
this.connection = connection;
this.publishDestination = publishDestination;
@ -167,11 +168,11 @@ namespace PettingZoo.UI.Tab.Publisher
Payload = Encoding.UTF8.GetString(receivedMessage.Body);
foreach (var header in receivedMessage.Properties.Headers)
foreach (var (key, value) in receivedMessage.Properties.Headers)
Headers.Add(new Header
{
Key = header.Key,
Value = header.Value
Key = key,
Value = value
});
PropertiesExpanded = AnyNotEmpty(AppId, ContentEncoding, Expiration, MessageId, Priority, Timestamp, TypeProperty, UserId);
@ -244,7 +245,7 @@ namespace PettingZoo.UI.Tab.Publisher
}
private bool PublishCanExecute()
private static bool PublishCanExecute()
{
// TODO validate input
return true;

View File

@ -4,18 +4,18 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:publisher="clr-namespace:PettingZoo.UI.Tab.Publisher"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls;assembly=PettingZoo.WPF"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="800"
d:DataContext="{d:DesignInstance publisher:DesignTimeTapetiPublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<ui:GridLayout Style="{StaticResource Form}">
<controls:GridLayout Style="{StaticResource Form}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:GridLayout.RowDefinitions>
<controls:GridLayout.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -23,7 +23,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
</ui:GridLayout.RowDefinitions>
</controls:GridLayout.RowDefinitions>
<Label Grid.Column="0" Content="{x:Static publisher:TapetiPublisherViewStrings.LabelProperties}" Style="{StaticResource SectionLabel}"/>
@ -42,12 +42,10 @@
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding ClassName, UpdateSourceTrigger=PropertyChanged}" GotFocus="CaretToEnd" />
<!--
<Button Grid.Column="1" Content="{x:Static publisher:TapetiPublisherViewStrings.ButtonBrowseClass}" />
-->
<Button Grid.Column="1" Content="{x:Static publisher:TapetiPublisherViewStrings.ButtonBrowseClass}" Command="{Binding BrowseClassCommand}" />
</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"/>
</ui:GridLayout>
</controls:GridLayout>
</UserControl>

View File

@ -1,21 +1,28 @@
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.WPF.ViewModel;
using IConnection = PettingZoo.Core.Connection.IConnection;
namespace PettingZoo.UI.Tab.Publisher
{
public class TapetiPublisherViewModel : BaseViewModel
public class TapetiPublisherViewModel : BaseViewModel, ITabHostWindowNotify
{
private readonly IConnection connection;
private readonly IPublishDestination publishDestination;
private readonly IExampleGenerator exampleGenerator;
private readonly DelegateCommand publishCommand;
private readonly DelegateCommand browseClassCommand;
private string correlationId = "";
private string payload = "";
private string className = "";
private string assemblyName = "";
private Window? tabHostWindow;
public string CorrelationId
@ -51,6 +58,7 @@ namespace PettingZoo.UI.Tab.Publisher
public ICommand PublishCommand => publishCommand;
public ICommand BrowseClassCommand => browseClassCommand;
@ -81,12 +89,14 @@ namespace PettingZoo.UI.Tab.Publisher
}
public TapetiPublisherViewModel(IConnection connection, IPublishDestination publishDestination, ReceivedMessageInfo? receivedMessage = null)
public TapetiPublisherViewModel(IConnection connection, IPublishDestination publishDestination, IExampleGenerator exampleGenerator, ReceivedMessageInfo? receivedMessage = null)
{
this.connection = connection;
this.publishDestination = publishDestination;
this.exampleGenerator = exampleGenerator;
publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
browseClassCommand = new DelegateCommand(BrowseClassExecute);
if (receivedMessage == null || !IsTapetiMessage(receivedMessage, out var receivedAssemblyName, out var receivedClassName))
@ -99,6 +109,29 @@ namespace PettingZoo.UI.Tab.Publisher
}
private void BrowseClassExecute()
{
exampleGenerator.Select(tabHostWindow, example =>
{
Dispatcher.CurrentDispatcher.BeginInvoke(() =>
{
switch (example)
{
case null:
return;
case IClassTypeExample classTypeExample:
AssemblyName = classTypeExample.AssemblyName;
ClassName = classTypeExample.FullClassName;
break;
}
Payload = example.Generate();
});
});
}
private void PublishExecute()
{
static string? NullIfEmpty(string? value)
@ -125,17 +158,22 @@ namespace PettingZoo.UI.Tab.Publisher
}
private bool PublishCanExecute()
private static bool PublishCanExecute()
{
// TODO validate input
return true;
}
public void HostWindowChanged(Window? hostWindow)
{
tabHostWindow = hostWindow;
}
}
public class DesignTimeTapetiPublisherViewModel : TapetiPublisherViewModel
{
public DesignTimeTapetiPublisherViewModel() : base(null!, null!)
public DesignTimeTapetiPublisherViewModel() : base(null!, null!, null!)
{
AssemblyName = "Messaging.Example";
ClassName = "Messaging.Example.ExampleMessage";

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows.Input;
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

View File

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Tab.Undocked
{
@ -17,7 +17,7 @@ namespace PettingZoo.UI.Tab.Undocked
public string Title => tab.Title;
public ContentControl Content => tab.Content;
public IEnumerable<TabToolbarCommand> ToolbarCommands => tab.ToolbarCommands;
public IEnumerable<TabToolbarCommand> ToolbarCommands => (tab as ITabToolbarCommands)?.ToolbarCommands ?? Enumerable.Empty<TabToolbarCommand>();
public Visibility ToolbarCommandsSeparatorVisibility =>
ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed;
@ -63,23 +63,12 @@ namespace PettingZoo.UI.Tab.Undocked
private class DesignTimeTab : ITab
{
#pragma warning disable CS0067 // "The event ... is never used" - it's part of the interface so it's required.
public event PropertyChangedEventHandler? PropertyChanged;
public IEnumerable<TabToolbarCommand> ToolbarCommands { get; } = Array.Empty<TabToolbarCommand>();
#pragma warning restore CS0067
public string Title => "Design-time tab title";
public ContentControl Content => null!;
public void Activate()
{
// Just to prevent the "PropertyChanged is never used" message
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
}
public void Deactivate()
{
}
}
}
}

View File

@ -3,12 +3,12 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace PettingZoo.UI.Tab
{
public class ViewTab<TView, TViewModel> : ITab where TView : ContentControl where TViewModel : INotifyPropertyChanged
public class ViewTab<TView, TViewModel> : ITab, ITabToolbarCommands, ITabActivate, ITabHostWindowNotify where TView : ContentControl where TViewModel : INotifyPropertyChanged
{
public string Title => getTitle(viewModel);
public ContentControl Content { get; }
@ -57,5 +57,11 @@ namespace PettingZoo.UI.Tab
{
(viewModel as ITabActivate)?.Deactivate();
}
public void HostWindowChanged(Window? hostWindow)
{
(viewModel as ITabHostWindowNotify)?.HostWindowChanged(hostWindow);
}
}
}

View File

@ -1,5 +1,5 @@
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Generator;
using PettingZoo.UI.Tab.Publisher;
using PettingZoo.UI.Tab.Subscriber;
@ -8,11 +8,13 @@ namespace PettingZoo.UI.Tab
public class ViewTabFactory : ITabFactory
{
private readonly ITabHost tabHost;
private readonly IExampleGenerator exampleGenerator;
public ViewTabFactory(ITabHost tabHost)
public ViewTabFactory(ITabHost tabHost, IExampleGenerator exampleGenerator)
{
this.tabHost = tabHost;
this.exampleGenerator = exampleGenerator;
}
@ -28,7 +30,7 @@ namespace PettingZoo.UI.Tab
public ITab CreatePublisherTab(IConnection connection, ReceivedMessageInfo? fromReceivedMessage = null)
{
var viewModel = new PublisherViewModel(tabHost, this, connection, fromReceivedMessage);
var viewModel = new PublisherViewModel(tabHost, this, connection, exampleGenerator, fromReceivedMessage);
return new ViewTab<PublisherView, PublisherViewModel>(
new PublisherView(viewModel),
viewModel,

View File

@ -0,0 +1,43 @@
using System;
namespace PettingZoo.UI
{
public readonly struct TextPosition : IEquatable<TextPosition>
{
public int Row { get; }
public int Column { get; }
public TextPosition(int row, int column)
{
Row = row;
Column = column;
}
public bool Equals(TextPosition other)
{
return Row == other.Row && Column == other.Column;
}
public override bool Equals(object? obj)
{
return obj is TextPosition other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Row, Column);
}
public static bool operator ==(TextPosition left, TextPosition right)
{
return left.Equals(right);
}
public static bool operator !=(TextPosition left, TextPosition right)
{
return !(left == right);
}
}
}

View File

@ -1,4 +1,4 @@
# ![Petting Zoo](https://raw.githubusercontent.com/PsychoMark/PettingZoo/master/Images/PettingZoo-48.png) Petting Zoo
# ![Petting Zoo](https://raw.githubusercontent.com/MvRens/PettingZoo/master/PettingZoo/Images/PettingZoo-48.png) Petting Zoo
##### A RabbitMQ live message viewer
ToDo: explain how it brings you coffee, fame and world peace. Or maybe just makes watching the messages flow slightly more comfortable.