WIP: generate examples from Tapeti message classes from NuGet feeds

- Implementation of assembly/nuget package selection UI
- Actual example generation needs more work
This commit is contained in:
Mark van Renswoude 2021-12-31 18:48:04 +01:00
parent b549729bf5
commit 133adf205c
73 changed files with 2603 additions and 490 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

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

@ -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.AppDataRoot, $"{databaseName}.litedb");
}

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.Reflection;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using PettingZoo.Core.Generator;
using Tapeti.DataAnnotations.Extensions;
namespace PettingZoo.Tapeti.AssemblyParser
{
public class AssemblyParser : IDisposable
{
private readonly MetadataLoadContext loadContext;
public AssemblyParser(params string[] extraAssemblies)
{
var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");
var paths = runtimeAssemblies
.Concat(extraAssemblies)
// TODO find a cleaner way
.Append(typeof(JsonSerializer).Assembly.Location)
.Append(typeof(RequiredGuidAttribute).Assembly.Location);
var resolver = new PathAssemblyResolver(paths);
loadContext = new MetadataLoadContext(resolver);
}
public void Dispose()
{
loadContext.Dispose();
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,42 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace PettingZoo.Tapeti.NuGet
{
// TODO support logger
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; }
// TODO support fetching dependencies
public Task Download(Stream destination, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,171 @@
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;
namespace PettingZoo.Tapeti.NuGet
{
public class NuGetPackageManager : INuGetPackageManager
{
private readonly SourceCacheContext cache;
private readonly List<Source> sources;
public IReadOnlyList<INuGetPackageSource> Sources => sources;
public NuGetPackageManager()
{
cache = new SourceCacheContext();
sources = new List<Source>
{
new(cache, "nuget.org", @"https://api.nuget.org/v3/index.json")
};
}
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(cache, nameAttribute.Value, urlAttribute.Value));
}
return this;
}
private class Source : INuGetPackageSource
{
private readonly SourceCacheContext cache;
private readonly SourceRepository repository;
public string Name { get; }
public Source(SourceCacheContext cache, string name, string url)
{
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>();
var resource = await repository.GetResourceAsync<PackageSearchResource>(cancellationToken);
var filter = new SearchFilter(includePrerelease);
return (await resource.SearchAsync(searchTerm, filter, 0, 20, new NullLogger(),
cancellationToken))
.Select(p => new Package(cache, repository, p))
.ToArray();
}
}
protected class Package : INuGetPackage
{
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(SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata)
{
this.cache = cache;
this.repository = repository;
this.packageSearchMetadata = packageSearchMetadata;
}
public async Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken)
{
return versions ??= (await packageSearchMetadata.GetVersionsAsync())
.Select(v => new PackageVersion(cache, repository, packageSearchMetadata, v.Version))
.ToArray();
}
}
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,69 @@
<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="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,111 @@
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;
namespace PettingZoo.Tapeti
{
public class TapetiClassLibraryExampleGenerator : IExampleGenerator
{
public void Select(object? ownerWindow, Action<IExample> onExampleSelected)
{
var packageManager = new NuGetPackageManager()
.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)
{
// TODO support folder with additional assemblies to load, or implement a proper NuGet install (with dependencies) instead
var assemblyParser = new AssemblyParser.AssemblyParser();
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,7 +6,15 @@ using Newtonsoft.Json.Linq;
namespace PettingZoo.Tapeti
{
internal class TypeToJObjectConverter
// TODO detect recursion
// TODO detect recursion
// TODO detect recursion
// TODO stop making nerdy jokes in comments.
// TODO generate at least one item for enumerables
// TODO support basic types
public class TypeToJObjectConverter
{
public static JObject Convert(Type type)
{
@ -63,7 +71,7 @@ namespace PettingZoo.Tapeti
}
if (actualType.IsArray)
return new JArray(Convert(actualType.GetElementType()));
return new JArray(Convert(actualType.GetElementType()!));
if (actualType.IsEnum)
return Enum.GetNames(actualType).FirstOrDefault();

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,418 @@
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;
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

@ -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,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<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,17 +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: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>
@ -26,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>
@ -40,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}}"/>

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,9 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.WPF", "PettingZoo.WPF\PettingZoo.WPF.csproj", "{E6617B69-2AC4-4056-B801-DD32E2374B71}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -44,6 +46,10 @@ 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
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

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

@ -22,6 +22,8 @@
<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,8 +44,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.1.2.30" />
<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" />
@ -51,6 +57,8 @@
<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>
@ -69,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>
@ -128,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.AppDataRoot, @"logs", "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,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

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

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();

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

@ -148,26 +148,47 @@ namespace PettingZoo.UI.Tab.Publisher
// https://stackoverflow.com/questions/18964176/two-way-binding-to-avalonedit-document-text-using-mvvm
Editor.Document.Text = Payload;
var editorTriggered = false;
Editor.TextChanged += (_, _) =>
{
Payload = Editor.Document.Text;
editorTriggered = true;
try
{
Payload = Editor.Document.Text;
}
finally
{
editorTriggered = false;
}
};
viewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName != nameof(viewModel.ValidationInfo))
if (args.PropertyName != nameof(viewModel.ValidationInfo) &&
(args.PropertyName != nameof(viewModel.Payload) || editorTriggered))
return;
Dispatcher.Invoke(() =>
{
if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
return;
switch (args.PropertyName)
{
case nameof(viewModel.ValidationInfo):
if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
return;
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
// TODO this can probably be optimized to only redraw the affected line
Editor.TextArea.TextView.Redraw();
// 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;
}
});
};

View File

@ -5,6 +5,7 @@ using System.Windows;
using ICSharpCode.AvalonEdit.Highlighting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PettingZoo.WPF.ViewModel;
namespace PettingZoo.UI.Tab.Publisher
{
@ -13,7 +14,7 @@ namespace PettingZoo.UI.Tab.Publisher
Json,
Plain,
Other
};
}
public enum ValidationStatus

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,31 @@ 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();
// TODO if validating example, keep reference for validation... and implement validation of course
});
});
}
private void PublishExecute()
{
static string? NullIfEmpty(string? value)
@ -125,17 +160,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,