Merge branch 'release/1.0'
This commit is contained in:
commit
dd9f089602
@ -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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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);
|
||||
|
||||
|
36
PettingZoo.Core/Settings/PettingZooPaths.cs
Normal file
36
PettingZoo.Core/Settings/PettingZooPaths.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace PettingZoo.Core.Settings
|
||||
{
|
||||
public static class PettingZooPaths
|
||||
{
|
||||
public static string AppDataRoot { get; }
|
||||
public static string InstallationRoot { get; }
|
||||
|
||||
public static string LogPath => Path.Combine(AppDataRoot, @"Logs");
|
||||
|
||||
public static string DatabasePath => AppDataRoot;
|
||||
|
||||
|
||||
public const string AssembliesPath = @"Assemblies";
|
||||
|
||||
public static string AppDataAssemblies => Path.Combine(AppDataRoot, AssembliesPath);
|
||||
public static string InstallationAssemblies => Path.Combine(InstallationRoot, AssembliesPath);
|
||||
|
||||
|
||||
static PettingZooPaths()
|
||||
{
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
if (appDataPath == null)
|
||||
throw new IOException("Could not resolve application data path");
|
||||
|
||||
AppDataRoot = Path.Combine(appDataPath, @"PettingZoo");
|
||||
if (!Directory.CreateDirectory(AppDataRoot).Exists)
|
||||
throw new IOException($"Failed to create directory: {AppDataRoot}");
|
||||
|
||||
InstallationRoot = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location ?? Assembly.GetExecutingAssembly().Location)!;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using LiteDB;
|
||||
using LiteDB.Async;
|
||||
using PettingZoo.Core.Settings;
|
||||
|
||||
namespace PettingZoo.Settings.LiteDB
|
||||
{
|
||||
@ -15,15 +16,7 @@ namespace PettingZoo.Settings.LiteDB
|
||||
|
||||
public BaseLiteDBRepository(string databaseName)
|
||||
{
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
if (appDataPath == null)
|
||||
throw new IOException("Could not resolve application data path");
|
||||
|
||||
var databasePath = Path.Combine(appDataPath, @"PettingZoo");
|
||||
if (!Directory.CreateDirectory(databasePath).Exists)
|
||||
throw new IOException($"Failed to create directory: {databasePath}");
|
||||
|
||||
databaseFilename = Path.Combine(databasePath, $"{databaseName}.litedb");
|
||||
databaseFilename = Path.Combine(PettingZooPaths.DatabasePath, $"{databaseName}.litedb");
|
||||
}
|
||||
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LiteDB" Version="5.0.11" />
|
||||
<PackageReference Include="LiteDB.Async" Version="0.0.11" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
45
PettingZoo.Tapeti/AssemblyLoader/FilePackageAssemblies.cs
Normal file
45
PettingZoo.Tapeti/AssemblyLoader/FilePackageAssemblies.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
PettingZoo.Tapeti/AssemblyLoader/IPackageAssemblies.cs
Normal file
19
PettingZoo.Tapeti/AssemblyLoader/IPackageAssemblies.cs
Normal 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();
|
||||
}
|
||||
}
|
82
PettingZoo.Tapeti/AssemblyLoader/NuGetPackageAssemblies.cs
Normal file
82
PettingZoo.Tapeti/AssemblyLoader/NuGetPackageAssemblies.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
83
PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs
Normal file
83
PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Loader;
|
||||
using Newtonsoft.Json;
|
||||
using PettingZoo.Core.Generator;
|
||||
|
||||
namespace PettingZoo.Tapeti.AssemblyParser
|
||||
{
|
||||
public class AssemblyParser : IDisposable
|
||||
{
|
||||
private readonly AssemblyLoadContext loadContext;
|
||||
|
||||
public AssemblyParser(params string[] extraAssembliesPaths)
|
||||
{
|
||||
// Using the MetadataLoadContext introduces extra complexity since types can not be compared
|
||||
// (a string from the loaded assembly does not equal our typeof(string) for example).
|
||||
// So instead we'll use a regular AssemblyLoadContext. Not ideal, and will probably cause other side-effects
|
||||
// if we're not careful, but I don't feel like writing a full metadata parser right now.
|
||||
// If you have a better idea, it's open-source! :-)
|
||||
loadContext = new AssemblyLoadContext(null, true);
|
||||
|
||||
foreach (var extraAssembly in extraAssembliesPaths.SelectMany(p => Directory.Exists(p)
|
||||
? Directory.GetFiles(p, "*.dll")
|
||||
: Enumerable.Empty<string>()))
|
||||
{
|
||||
loadContext.LoadFromAssemblyPath(extraAssembly);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
loadContext.Unload();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<IClassTypeExample> GetExamples(Stream assemblyStream)
|
||||
{
|
||||
var assembly = loadContext.LoadFromStream(assemblyStream);
|
||||
|
||||
foreach (var type in assembly.GetTypes().Where(t => t.IsClass))
|
||||
yield return new TypeExample(type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class TypeExample : IClassTypeExample
|
||||
{
|
||||
public string AssemblyName => type.Assembly.GetName().Name ?? "";
|
||||
public string? Namespace => type.Namespace;
|
||||
public string ClassName => type.Name;
|
||||
|
||||
private readonly Type type;
|
||||
|
||||
|
||||
public TypeExample(Type type)
|
||||
{
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
public string Generate()
|
||||
{
|
||||
/*
|
||||
We can't create an instance of the type to serialize easily, as most will depend on
|
||||
assemblies not included in the NuGet package, so we'll parse the Type ourselves.
|
||||
This is still much easier than using MetadataReader, as we can more easily check against
|
||||
standard types like Nullable.
|
||||
|
||||
The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models
|
||||
themselves should not inherit from classes outside of their assembly, or include properties
|
||||
with types from other assemblies. With that assumption, walking the class structure should be safe.
|
||||
The extraAssemblies passed to TapetiClassLibraryExampleSource can also be used to give it a better chance.
|
||||
*/
|
||||
var serialized = TypeToJObjectConverter.Convert(type);
|
||||
return serialized.ToString(Formatting.Indented);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs
Normal file
40
PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PettingZoo.Tapeti.NuGet
|
||||
{
|
||||
public interface INuGetPackageManager
|
||||
{
|
||||
public IReadOnlyList<INuGetPackageSource> Sources { get; }
|
||||
}
|
||||
|
||||
|
||||
public interface INuGetPackageSource
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public Task<IReadOnlyList<INuGetPackage>> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
public interface INuGetPackage
|
||||
{
|
||||
public string Title { get; }
|
||||
public string Description { get; }
|
||||
public string Authors { get; }
|
||||
public string Version { get; }
|
||||
|
||||
public Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
public interface INuGetPackageVersion : IComparable<INuGetPackageVersion>
|
||||
{
|
||||
public string Version { get; }
|
||||
|
||||
public Task Download(Stream destination, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
198
PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs
Normal file
198
PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs
Normal file
@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using NuGet.Common;
|
||||
using NuGet.Protocol;
|
||||
using NuGet.Protocol.Core.Types;
|
||||
using NuGet.Versioning;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace PettingZoo.Tapeti.NuGet
|
||||
{
|
||||
public class NuGetPackageManager : INuGetPackageManager
|
||||
{
|
||||
private const string NuGetDefaultSource = @"https://api.nuget.org/v3/index.json";
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly SourceCacheContext cache;
|
||||
private readonly List<Source> sources;
|
||||
|
||||
public IReadOnlyList<INuGetPackageSource> Sources => sources;
|
||||
|
||||
|
||||
public NuGetPackageManager(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
cache = new SourceCacheContext();
|
||||
sources = new List<Source>
|
||||
{
|
||||
new(logger.ForContext("source", NuGetDefaultSource), cache, "nuget.org", NuGetDefaultSource)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public NuGetPackageManager WithSourcesFrom(string nuGetConfig)
|
||||
{
|
||||
if (!File.Exists(nuGetConfig))
|
||||
return this;
|
||||
|
||||
var doc = new XmlDocument();
|
||||
doc.Load(nuGetConfig);
|
||||
|
||||
var nodes = doc.SelectNodes(@"/configuration/packageSources/add");
|
||||
if (nodes == null)
|
||||
return this;
|
||||
|
||||
foreach (var entry in nodes.Cast<XmlNode>())
|
||||
{
|
||||
if (entry.Attributes == null)
|
||||
continue;
|
||||
|
||||
var nameAttribute = entry.Attributes["key"];
|
||||
var urlAttribute = entry.Attributes["value"];
|
||||
|
||||
if (string.IsNullOrEmpty(nameAttribute?.Value) || string.IsNullOrEmpty(urlAttribute?.Value))
|
||||
continue;
|
||||
|
||||
sources.Add(new Source(logger.ForContext("source", urlAttribute.Value), cache, nameAttribute.Value, urlAttribute.Value));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class Source : INuGetPackageSource
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly SourceCacheContext cache;
|
||||
private readonly SourceRepository repository;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
|
||||
public Source(ILogger logger, SourceCacheContext cache, string name, string url)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.cache = cache;
|
||||
Name = name;
|
||||
repository = Repository.Factory.GetCoreV3(url);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IReadOnlyList<INuGetPackage>> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
return Array.Empty<INuGetPackage>();
|
||||
|
||||
try
|
||||
{
|
||||
var resource = await repository.GetResourceAsync<PackageSearchResource>(cancellationToken);
|
||||
var filter = new SearchFilter(includePrerelease);
|
||||
|
||||
var result = (await resource.SearchAsync(searchTerm, filter, 0, 20, new NullLogger(),
|
||||
cancellationToken))
|
||||
.Select(p => new Package(logger, cache, repository, p))
|
||||
.ToArray();
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "NuGet Search failed for term '{searchTerm}' (includePrerelease {includePrerelease})", searchTerm, includePrerelease);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class Package : INuGetPackage
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly SourceCacheContext cache;
|
||||
private readonly SourceRepository repository;
|
||||
private readonly IPackageSearchMetadata packageSearchMetadata;
|
||||
|
||||
public string Title => packageSearchMetadata.Title;
|
||||
public string Description => packageSearchMetadata.Description;
|
||||
public string Authors => packageSearchMetadata.Authors;
|
||||
public string Version => packageSearchMetadata.Identity.Version.ToString();
|
||||
|
||||
|
||||
private IReadOnlyList<INuGetPackageVersion>? versions;
|
||||
|
||||
|
||||
public Package(ILogger logger, SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.cache = cache;
|
||||
this.repository = repository;
|
||||
this.packageSearchMetadata = packageSearchMetadata;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return versions ??= (await packageSearchMetadata.GetVersionsAsync())
|
||||
.Select(v => new PackageVersion(cache, repository, packageSearchMetadata, v.Version))
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "NuGet GetVersions failed for packge Id '{packageId}')", packageSearchMetadata.Identity.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class PackageVersion : INuGetPackageVersion, IComparable<PackageVersion>
|
||||
{
|
||||
private readonly SourceCacheContext cache;
|
||||
private readonly SourceRepository repository;
|
||||
private readonly IPackageSearchMetadata packageSearchMetadata;
|
||||
|
||||
protected readonly NuGetVersion NuGetVersion;
|
||||
|
||||
|
||||
public PackageVersion(SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata, NuGetVersion nuGetVersion)
|
||||
{
|
||||
this.cache = cache;
|
||||
this.repository = repository;
|
||||
this.packageSearchMetadata = packageSearchMetadata;
|
||||
NuGetVersion = nuGetVersion;
|
||||
}
|
||||
|
||||
|
||||
public string Version => NuGetVersion.ToString();
|
||||
|
||||
|
||||
public async Task Download(Stream destination, CancellationToken cancellationToken)
|
||||
{
|
||||
var resource = await repository.GetResourceAsync<FindPackageByIdResource>(cancellationToken);
|
||||
await resource.CopyNupkgToStreamAsync(packageSearchMetadata.Identity.Id, NuGetVersion, destination, cache, new NullLogger(), cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
public int CompareTo(INuGetPackageVersion? other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return 0;
|
||||
if (other == null) return 1;
|
||||
|
||||
return other is PackageVersion packageVersion ? CompareTo(packageVersion) : string.Compare(Version, other.Version, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int CompareTo(PackageVersion? other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return 0;
|
||||
return other == null ? 1 : NuGetVersion.CompareTo(other.NuGetVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,70 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Version>0.1</Version>
|
||||
<UseWpf>true</UseWpf>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NuGet.Packaging" Version="6.0.0" />
|
||||
<PackageReference Include="NuGet.Protocol" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="SharpVectors" Version="1.7.7" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="6.0.0" />
|
||||
<PackageReference Include="Tapeti.DataAnnotations.Extensions" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PettingZoo.Core\PettingZoo.Core.csproj" />
|
||||
<ProjectReference Include="..\PettingZoo.WPF\PettingZoo.WPF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="UI\ClassSelection\ClassSelectionStrings.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>ClassSelectionStrings.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\PackageProgress\PackageProgressStrings.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>PackageProgressStrings.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\PackageProgress\PackageProgressWindow.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="UI\PackageSelection\PackageSelectionStrings.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>PackageSelectionStrings.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="UI\ClassSelection\ClassSelectionStrings.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>ClassSelectionStrings.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="UI\PackageProgress\PackageProgressStrings.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>PackageProgressStrings.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="UI\PackageSelection\PackageSelectionStrings.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>PackageSelectionStrings.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="UI\PackageProgress\PackageProgressWindow.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
124
PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs
Normal file
124
PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.Core.Settings;
|
||||
using PettingZoo.Tapeti.AssemblyLoader;
|
||||
using PettingZoo.Tapeti.NuGet;
|
||||
using PettingZoo.Tapeti.UI.ClassSelection;
|
||||
using PettingZoo.Tapeti.UI.PackageProgress;
|
||||
using PettingZoo.Tapeti.UI.PackageSelection;
|
||||
using Serilog;
|
||||
|
||||
namespace PettingZoo.Tapeti
|
||||
{
|
||||
public class TapetiClassLibraryExampleGenerator : IExampleGenerator
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
|
||||
public TapetiClassLibraryExampleGenerator(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public void Select(object? ownerWindow, Action<IExample> onExampleSelected)
|
||||
{
|
||||
var packageManager = new NuGetPackageManager(logger)
|
||||
.WithSourcesFrom(Path.Combine(PettingZooPaths.InstallationRoot, @"nuget.config"))
|
||||
.WithSourcesFrom(Path.Combine(PettingZooPaths.AppDataRoot, @"nuget.config"));
|
||||
|
||||
var dispatcher = Dispatcher.CurrentDispatcher;
|
||||
|
||||
var viewModel = new PackageSelectionViewModel(packageManager);
|
||||
var selectionWindow = new PackageSelectionWindow(viewModel)
|
||||
{
|
||||
Owner = ownerWindow as Window
|
||||
};
|
||||
|
||||
viewModel.Select += (_, args) =>
|
||||
{
|
||||
dispatcher.Invoke(() =>
|
||||
{
|
||||
var windowBounds = selectionWindow.RestoreBounds;
|
||||
selectionWindow.Close();
|
||||
|
||||
var progressWindow = new PackageProgressWindow();
|
||||
progressWindow.Left = windowBounds.Left + (windowBounds.Width - progressWindow.Width) / 2;
|
||||
progressWindow.Left = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2;
|
||||
progressWindow.Show();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO allow cancelling (by closing the progress window and optionally a Cancel button)
|
||||
var assemblies = await args.Assemblies.GetAssemblies(progressWindow, CancellationToken.None);
|
||||
|
||||
// var classes =
|
||||
var examples = LoadExamples(assemblies);
|
||||
|
||||
dispatcher.Invoke(() =>
|
||||
{
|
||||
progressWindow.Close();
|
||||
progressWindow = null;
|
||||
|
||||
var classSelectionViewModel = new ClassSelectionViewModel(examples);
|
||||
var classSelectionWindow = new ClassSelectionWindow(classSelectionViewModel)
|
||||
{
|
||||
Top = windowBounds.Top,
|
||||
Left = windowBounds.Left,
|
||||
Width = windowBounds.Width,
|
||||
Height = windowBounds.Height
|
||||
};
|
||||
|
||||
classSelectionViewModel.Select += (_, example) =>
|
||||
{
|
||||
classSelectionWindow.Close();
|
||||
onExampleSelected(example);
|
||||
};
|
||||
|
||||
classSelectionWindow.ShowDialog();
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
dispatcher.Invoke(() =>
|
||||
{
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier - if I remove it, there's a "Dereference of a possibly null reference" warning instead
|
||||
progressWindow?.Close();
|
||||
|
||||
MessageBox.Show($"Error while loading assembly: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
selectionWindow.ShowDialog();
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<IClassTypeExample> LoadExamples(IEnumerable<IPackageAssembly> assemblies)
|
||||
{
|
||||
var assemblyParser = new AssemblyParser.AssemblyParser(
|
||||
PettingZooPaths.AppDataAssemblies,
|
||||
PettingZooPaths.InstallationAssemblies
|
||||
);
|
||||
|
||||
return assemblies
|
||||
.SelectMany(a =>
|
||||
{
|
||||
using var stream = a.GetStream();
|
||||
return assemblyParser.GetExamples(stream).ToArray();
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,18 +6,47 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PettingZoo.Tapeti
|
||||
{
|
||||
internal class TypeToJObjectConverter
|
||||
public class TypeToJObjectConverter
|
||||
{
|
||||
public static JObject Convert(Type type)
|
||||
{
|
||||
if (!type.IsClass)
|
||||
throw new ArgumentException($"TypeToJObjectConverter.Convert expects a class, got {type.Name}");
|
||||
|
||||
return ClassToJToken(type, Array.Empty<Type>());
|
||||
}
|
||||
|
||||
|
||||
private static readonly Dictionary<Type, Type> TypeEquivalenceMap = new()
|
||||
{
|
||||
{ typeof(uint), typeof(int) },
|
||||
{ typeof(long), typeof(int) },
|
||||
{ typeof(ulong), typeof(int) },
|
||||
{ typeof(short), typeof(int) },
|
||||
{ typeof(ushort), typeof(int) },
|
||||
{ typeof(float), typeof(decimal) }
|
||||
};
|
||||
|
||||
|
||||
private static readonly Dictionary<Type, JToken> TypeValueMap = new()
|
||||
{
|
||||
{ typeof(int), 0 },
|
||||
{ typeof(decimal), 0.0 },
|
||||
{ typeof(bool), false }
|
||||
};
|
||||
|
||||
|
||||
private static JObject ClassToJToken(Type classType, IEnumerable<Type> typesEncountered)
|
||||
{
|
||||
var newTypesEncountered = typesEncountered.Append(classType).ToArray();
|
||||
var result = new JObject();
|
||||
|
||||
foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
foreach (var propertyInfo in classType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
// Note: unfortunately we can not call GetCustomAttributes here, as that would
|
||||
// trigger assemblies not included in the package to be loaded
|
||||
// Note: unfortunately we can not call GetCustomAttributes here for now, as that would
|
||||
// trigger assemblies not included in the package to be loaded, which may not exist
|
||||
|
||||
var value = PropertyToJToken(propertyInfo.PropertyType);
|
||||
var value = TypeToJToken(propertyInfo.PropertyType, newTypesEncountered);
|
||||
result.Add(propertyInfo.Name, value);
|
||||
}
|
||||
|
||||
@ -25,23 +54,12 @@ namespace PettingZoo.Tapeti
|
||||
}
|
||||
|
||||
|
||||
private static readonly Dictionary<Type, JToken> TypeMap = new()
|
||||
private static JToken TypeToJToken(Type type, ICollection<Type> typesEncountered)
|
||||
{
|
||||
{ typeof(short), 0 },
|
||||
{ typeof(ushort), 0 },
|
||||
{ typeof(int), 0 },
|
||||
{ typeof(uint), 0 },
|
||||
{ typeof(long), 0 },
|
||||
{ typeof(ulong), 0 },
|
||||
{ typeof(decimal), 0.0 },
|
||||
{ typeof(float), 0.0 },
|
||||
{ typeof(bool), false }
|
||||
};
|
||||
var actualType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
|
||||
private static JToken PropertyToJToken(Type propertyType)
|
||||
{
|
||||
var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
if (TypeEquivalenceMap.TryGetValue(actualType, out var equivalentType))
|
||||
actualType = equivalentType;
|
||||
|
||||
|
||||
// String is also a class
|
||||
@ -56,14 +74,13 @@ namespace PettingZoo.Tapeti
|
||||
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
|
||||
if (enumerableInterface != null)
|
||||
return new JArray(Convert(enumerableInterface.GetGenericArguments()[0]));
|
||||
return new JArray(TypeToJToken(enumerableInterface.GetGenericArguments()[0], typesEncountered));
|
||||
|
||||
|
||||
return Convert(actualType);
|
||||
return typesEncountered.Contains(actualType) ? new JValue((object?)null) : ClassToJToken(actualType, typesEncountered);
|
||||
}
|
||||
|
||||
if (actualType.IsArray)
|
||||
return new JArray(Convert(actualType.GetElementType()));
|
||||
return new JArray(TypeToJToken(actualType.GetElementType()!, typesEncountered));
|
||||
|
||||
if (actualType.IsEnum)
|
||||
return Enum.GetNames(actualType).FirstOrDefault();
|
||||
@ -80,11 +97,9 @@ namespace PettingZoo.Tapeti
|
||||
if (actualType == typeof(Guid))
|
||||
return Guid.NewGuid().ToString();
|
||||
|
||||
return TypeMap.TryGetValue(actualType, out var mappedToken)
|
||||
return TypeValueMap.TryGetValue(actualType, out var mappedToken)
|
||||
? mappedToken
|
||||
: $"(unknown type: {actualType.Name})";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
90
PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.Designer.cs
generated
Normal file
90
PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.Designer.cs
generated
Normal file
@ -0,0 +1,90 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace PettingZoo.Tapeti.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.resx
Normal file
129
PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.resx
Normal file
@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=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>
|
211
PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionViewModel.cs
Normal file
211
PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionViewModel.cs
Normal 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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
171
PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionStrings.Designer.cs
generated
Normal file
171
PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionStrings.Designer.cs
generated
Normal 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's installation directory "{0}" or in "{1}".
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,419 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Win32;
|
||||
using PettingZoo.Core.Settings;
|
||||
using PettingZoo.Tapeti.AssemblyLoader;
|
||||
using PettingZoo.Tapeti.NuGet;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
|
||||
namespace PettingZoo.Tapeti.UI.PackageSelection
|
||||
{
|
||||
public enum PackageSelectionSource
|
||||
{
|
||||
Assembly,
|
||||
NuGet
|
||||
}
|
||||
|
||||
|
||||
public class PackageSelectionViewModel : BaseViewModel
|
||||
{
|
||||
private readonly INuGetPackageManager nuGetPackageManager;
|
||||
|
||||
private readonly DelegateCommand selectCommand;
|
||||
|
||||
private PackageSelectionSource packageSelectionSource = PackageSelectionSource.Assembly;
|
||||
private string assemblyFilename = "";
|
||||
private readonly DelegateCommand assemblyBrowse;
|
||||
|
||||
private string nuGetSearchTerm = "";
|
||||
private bool nuGetIncludePrerelease;
|
||||
private INuGetPackageSource? selectedNuGetSource;
|
||||
private INuGetPackage? selectedPackage;
|
||||
private INuGetPackageVersion? selectedVersion;
|
||||
|
||||
private string packagesStatus = "";
|
||||
private Visibility packagesStatusVisibility = Visibility.Collapsed;
|
||||
|
||||
|
||||
public ICommand SelectCommand => selectCommand;
|
||||
|
||||
|
||||
public PackageSelectionSource PackageSelectionSource
|
||||
{
|
||||
get => packageSelectionSource;
|
||||
set => SetField(ref packageSelectionSource, value, otherPropertiesChanged: new[] { nameof(PackageSelectionSourceAssembly), nameof(PackageSelectionSourceNuGet) });
|
||||
}
|
||||
|
||||
|
||||
public bool PackageSelectionSourceAssembly
|
||||
{
|
||||
get => PackageSelectionSource == PackageSelectionSource.Assembly;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
PackageSelectionSource = PackageSelectionSource.Assembly;
|
||||
}
|
||||
}
|
||||
|
||||
public bool PackageSelectionSourceNuGet
|
||||
{
|
||||
get => PackageSelectionSource == PackageSelectionSource.NuGet;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
PackageSelectionSource = PackageSelectionSource.NuGet;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string AssemblyFilename
|
||||
{
|
||||
get => assemblyFilename;
|
||||
set
|
||||
{
|
||||
if (!SetField(ref assemblyFilename, value))
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
PackageSelectionSource = PackageSelectionSource.Assembly;
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand AssemblyBrowse => assemblyBrowse;
|
||||
|
||||
|
||||
// TODO hint for extra assemblies path
|
||||
public static string HintNuGetSources => string.Format(PackageSelectionStrings.HintNuGetSources, PettingZooPaths.InstallationRoot, PettingZooPaths.AppDataRoot);
|
||||
|
||||
public string NuGetSearchTerm
|
||||
{
|
||||
get => nuGetSearchTerm;
|
||||
set
|
||||
{
|
||||
if (!SetField(ref nuGetSearchTerm, value, otherPropertiesChanged: new[] { nameof(NuGetSearchTermPlaceholderVisibility) }))
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
PackageSelectionSource = PackageSelectionSource.NuGet;
|
||||
}
|
||||
}
|
||||
|
||||
public bool NuGetIncludePrerelease
|
||||
{
|
||||
get => nuGetIncludePrerelease;
|
||||
set => SetField(ref nuGetIncludePrerelease, value);
|
||||
}
|
||||
|
||||
public Visibility NuGetSearchTermPlaceholderVisibility => string.IsNullOrEmpty(NuGetSearchTerm) ? Visibility.Visible : Visibility.Hidden;
|
||||
public IReadOnlyList<INuGetPackageSource> NuGetSources => nuGetPackageManager.Sources;
|
||||
|
||||
public INuGetPackageSource? SelectedNuGetSource
|
||||
{
|
||||
get => selectedNuGetSource;
|
||||
set
|
||||
{
|
||||
if (!SetField(ref selectedNuGetSource, value))
|
||||
return;
|
||||
|
||||
Packages.Clear();
|
||||
SelectedPackage = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollectionEx<INuGetPackage> Packages { get; } = new();
|
||||
public ObservableCollectionEx<INuGetPackageVersion> Versions { get; } = new();
|
||||
|
||||
|
||||
public string PackagesStatus
|
||||
{
|
||||
get => packagesStatus;
|
||||
set => SetField(ref packagesStatus, value);
|
||||
}
|
||||
|
||||
|
||||
public Visibility PackagesStatusVisibility
|
||||
{
|
||||
get => packagesStatusVisibility;
|
||||
set => SetField(ref packagesStatusVisibility, value);
|
||||
}
|
||||
|
||||
|
||||
public INuGetPackage? SelectedPackage
|
||||
{
|
||||
get => selectedPackage;
|
||||
set
|
||||
{
|
||||
if (!SetField(ref selectedPackage, value))
|
||||
return;
|
||||
|
||||
Versions.Clear();
|
||||
SelectedVersion = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public INuGetPackageVersion? SelectedVersion
|
||||
{
|
||||
get => selectedVersion;
|
||||
set => SetField(ref selectedVersion, value);
|
||||
}
|
||||
|
||||
|
||||
public class SelectPackageEventArgs
|
||||
{
|
||||
public IPackageAssemblies Assemblies { get; }
|
||||
|
||||
|
||||
public SelectPackageEventArgs(IPackageAssemblies assemblies)
|
||||
{
|
||||
Assemblies = assemblies;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public event EventHandler<SelectPackageEventArgs>? Select;
|
||||
|
||||
|
||||
public PackageSelectionViewModel(INuGetPackageManager nuGetPackageManager)
|
||||
{
|
||||
this.nuGetPackageManager = nuGetPackageManager;
|
||||
|
||||
selectCommand = new DelegateCommand(SelectExecute, SelectCanExecute);
|
||||
|
||||
assemblyBrowse = new DelegateCommand(AssemblyBrowseExecute);
|
||||
|
||||
|
||||
// TODO remember source
|
||||
if (nuGetPackageManager.Sources.Count > 0)
|
||||
selectedNuGetSource = nuGetPackageManager.Sources[^1];
|
||||
|
||||
var propertyChangedObservable = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
|
||||
h => PropertyChanged += h,
|
||||
h => PropertyChanged -= h);
|
||||
|
||||
propertyChangedObservable
|
||||
.Where(e => e.EventArgs.PropertyName is nameof(NuGetSearchTerm) or nameof(NuGetIncludePrerelease) or nameof(SelectedNuGetSource))
|
||||
.Throttle(TimeSpan.FromMilliseconds(500))
|
||||
.ObserveOn(SynchronizationContext.Current!)
|
||||
.Subscribe(_ => NuGetSearch());
|
||||
|
||||
propertyChangedObservable
|
||||
.Where(e => e.EventArgs.PropertyName == nameof(SelectedPackage))
|
||||
.Throttle(TimeSpan.FromMilliseconds(100))
|
||||
.ObserveOn(SynchronizationContext.Current!)
|
||||
.Subscribe(_ => NuGetGetVersions());
|
||||
|
||||
|
||||
propertyChangedObservable
|
||||
.Where(e => e.EventArgs.PropertyName is nameof(PackageSelectionSource) or nameof(AssemblyFilename) or nameof(SelectedVersion))
|
||||
.ObserveOn(SynchronizationContext.Current!)
|
||||
.Subscribe(_ => selectCommand.RaiseCanExecuteChanged());
|
||||
}
|
||||
|
||||
|
||||
private void SelectExecute()
|
||||
{
|
||||
IPackageAssemblies? assemblies = PackageSelectionSource switch
|
||||
{
|
||||
PackageSelectionSource.Assembly => !string.IsNullOrWhiteSpace(AssemblyFilename) ? new FilePackageAssemblies(AssemblyFilename) : null,
|
||||
PackageSelectionSource.NuGet => SelectedVersion != null ? new NuGetPackageAssemblies(SelectedVersion) : null,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
if (assemblies != null)
|
||||
Select?.Invoke(this, new SelectPackageEventArgs(assemblies));
|
||||
}
|
||||
|
||||
|
||||
private bool SelectCanExecute()
|
||||
{
|
||||
return PackageSelectionSource switch
|
||||
{
|
||||
PackageSelectionSource.Assembly => !string.IsNullOrWhiteSpace(AssemblyFilename),
|
||||
PackageSelectionSource.NuGet => SelectedVersion != null,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private void AssemblyBrowseExecute()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
CheckFileExists = true,
|
||||
Filter = PackageSelectionStrings.AssemblyFileFilter
|
||||
};
|
||||
|
||||
if (!dialog.ShowDialog().GetValueOrDefault())
|
||||
return;
|
||||
|
||||
AssemblyFilename = dialog.FileName;
|
||||
}
|
||||
|
||||
|
||||
private CancellationTokenSource? nuGetSearchCancellationTokenSource;
|
||||
|
||||
private void NuGetSearch()
|
||||
{
|
||||
if (SelectedNuGetSource == null)
|
||||
return;
|
||||
|
||||
nuGetSearchCancellationTokenSource?.Cancel();
|
||||
|
||||
nuGetSearchCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = nuGetSearchCancellationTokenSource.Token;
|
||||
|
||||
var source = SelectedNuGetSource;
|
||||
var searchTerm = NuGetSearchTerm;
|
||||
var includePrerelease = NuGetIncludePrerelease;
|
||||
|
||||
SelectedPackage = null;
|
||||
Packages.Clear();
|
||||
|
||||
PackagesStatus = PackageSelectionStrings.Loading;
|
||||
PackagesStatusVisibility = Visibility.Visible;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var packages = await source.Search(searchTerm, includePrerelease, cancellationToken);
|
||||
|
||||
await Application.Current.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
Packages.ReplaceAll(packages);
|
||||
SelectedPackage = null;
|
||||
|
||||
PackagesStatus = "";
|
||||
PackagesStatusVisibility = Visibility.Collapsed;
|
||||
|
||||
Versions.Clear();
|
||||
SelectedVersion = null;
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// By design...
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PackagesStatus = e.Message;
|
||||
PackagesStatusVisibility = Visibility.Visible;
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
private CancellationTokenSource? nuGetGetVersionsCancellationTokenSource;
|
||||
|
||||
private void NuGetGetVersions()
|
||||
{
|
||||
if (SelectedPackage == null)
|
||||
return;
|
||||
|
||||
nuGetGetVersionsCancellationTokenSource?.Cancel();
|
||||
|
||||
nuGetGetVersionsCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = nuGetGetVersionsCancellationTokenSource.Token;
|
||||
|
||||
var package = SelectedPackage;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var versions = await package.GetVersions(cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await Application.Current.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
Versions.ReplaceAll(versions);
|
||||
SelectedVersion = versions.Count > 0 ? versions[0] : null;
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// By design...
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class DesignTimePackageSelectionViewModel : PackageSelectionViewModel
|
||||
{
|
||||
public DesignTimePackageSelectionViewModel() : base(new DesignTimeNuGetPackageManager())
|
||||
{
|
||||
Packages.ReplaceAll(new []
|
||||
{
|
||||
new DesignTimeNuGetPackage("Tapeti", "Shameless plug", "M. van Renswoude", "2.8"),
|
||||
new DesignTimeNuGetPackage("Messaging.Example", "Some messaging package with a very long description to test the text trimming. It should be very very very very very very very very very very very long indeed.", "Anonymoose", "0.9")
|
||||
});
|
||||
|
||||
PackagesStatus = @"This is a very long status message, which is not unreasonable since exceptions can occur while fetching NuGet packages and they will be displayed here.";
|
||||
PackagesStatusVisibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
|
||||
private class DesignTimeNuGetPackageManager : INuGetPackageManager
|
||||
{
|
||||
public IReadOnlyList<INuGetPackageSource> Sources { get; } = new[]
|
||||
{
|
||||
new DesignTimeNuGetPackageSource("nuget.org")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private class DesignTimeNuGetPackageSource : INuGetPackageSource
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
|
||||
public DesignTimeNuGetPackageSource(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
|
||||
public Task<IReadOnlyList<INuGetPackage>> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(Array.Empty<INuGetPackage>() as IReadOnlyList<INuGetPackage>);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class DesignTimeNuGetPackage : INuGetPackage
|
||||
{
|
||||
public string Title { get; }
|
||||
public string Description { get; }
|
||||
public string Authors { get; }
|
||||
public string Version { get; }
|
||||
|
||||
|
||||
public DesignTimeNuGetPackage(string title, string description, string authors, string version)
|
||||
{
|
||||
Title = title;
|
||||
Description = description;
|
||||
Authors = authors;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
|
||||
public Task<IReadOnlyList<INuGetPackageVersion>> GetVersions(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(Array.Empty<INuGetPackageVersion>() as IReadOnlyList<INuGetPackageVersion>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
24
PettingZoo.Test/PettingZoo.Test.csproj
Normal file
24
PettingZoo.Test/PettingZoo.Test.csproj
Normal file
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.2.0" />
|
||||
<PackageReference Include="FluentAssertions.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PettingZoo.Tapeti\PettingZoo.Tapeti.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
65
PettingZoo.Test/Tapeti/TypeToJObjectTest.cs
Normal file
65
PettingZoo.Test/Tapeti/TypeToJObjectTest.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using FluentAssertions.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PettingZoo.Tapeti;
|
||||
using Xunit;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PettingZoo.Test.Tapeti
|
||||
{
|
||||
public class TypeToJObjectTest
|
||||
{
|
||||
[Fact]
|
||||
public void TestAllSupportedTypes()
|
||||
{
|
||||
var converted = TypeToJObjectConverter.Convert(typeof(AllSupportedTypesTest));
|
||||
|
||||
using (new AssertionScope())
|
||||
{
|
||||
// Directly supported
|
||||
converted.Should().HaveElement("StringValue").Which.Should().HaveValue("");
|
||||
converted.Should().HaveElement("IntValue").Which.Should().Match(t => t.Type == JTokenType.Integer);
|
||||
converted.Should().HaveElement("BoolValue").Which.Should().Match(t => t.Type == JTokenType.Boolean);
|
||||
|
||||
var guidValue = converted.Should().HaveElement("GuidValue").Which.Should().Match(t => t.Type == JTokenType.String)
|
||||
.And.Subject.Value<string>();
|
||||
Guid.TryParse(guidValue, out _).Should().BeTrue();
|
||||
|
||||
|
||||
var objectValue = converted.Should().HaveElement("ObjectValue").Subject;
|
||||
objectValue.Should().HaveElement("SubStringValue").Which.Should().HaveValue("");
|
||||
objectValue.Should().HaveElement("SubIntValue").Which.Should().Match(t => t.Type == JTokenType.Integer);
|
||||
objectValue.Should().HaveElement("RecursiveValue").Which.Type.Should().Be(JTokenType.Null);
|
||||
|
||||
// Via type mapping
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ReSharper disable UnusedMember.Local
|
||||
private class AllSupportedTypesTest
|
||||
{
|
||||
public string StringValue { get; set; }
|
||||
public int IntValue { get; set; }
|
||||
public bool BoolValue { get; set; }
|
||||
|
||||
public Guid GuidValue { get; set; }
|
||||
|
||||
public ClassProperty ObjectValue { get; set; }
|
||||
}
|
||||
|
||||
|
||||
// ReSharper disable once ClassNeverInstantiated.Local
|
||||
private class ClassProperty
|
||||
{
|
||||
public string SubStringValue { get; set; }
|
||||
public int SubIntValue { get; set; }
|
||||
|
||||
public AllSupportedTypesTest RecursiveValue { get; set; }
|
||||
}
|
||||
// ReSharper restore UnusedMember.Local
|
||||
}
|
||||
}
|
@ -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
|
||||
|
25
PettingZoo.WPF/PettingZoo.WPF.csproj
Normal file
25
PettingZoo.WPF/PettingZoo.WPF.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AvalonEdit" Version="6.1.3.50" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="ICSharpCode.AvalonEdit">
|
||||
<HintPath>C:\Users\PsychoMark\.nuget\packages\avalonedit\6.1.2.30\lib\net5.0-windows7.0\ICSharpCode.AvalonEdit.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Style.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,16 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="clr-namespace:PettingZoo.UI">
|
||||
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
|
||||
xmlns:controls="clr-namespace:PettingZoo.WPF.Controls">
|
||||
<!-- Global styling -->
|
||||
<Style x:Key="WindowStyle" TargetType="{x:Type Window}">
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type Button}">
|
||||
<Style x:Key="{x:Type Button}" TargetType="{x:Type Button}">
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type TextBox}">
|
||||
<Style x:Key="{x:Type TextBox}" TargetType="{x:Type TextBox}">
|
||||
<Setter Property="Padding" Value="3" />
|
||||
</Style>
|
||||
|
||||
@ -25,7 +26,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.InactiveCaptionBrushKey}}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveCaptionTextBrushKey}}"/>
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="FooterPanel" TargetType="{x:Type Panel}">
|
||||
<Setter Property="Margin" Value="0,8,0,0" />
|
||||
</Style>
|
||||
@ -39,10 +40,17 @@
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="Form" TargetType="{x:Type ui:GridLayout}">
|
||||
<Style x:Key="Form" TargetType="{x:Type controls:GridLayout}">
|
||||
<Setter Property="ChildMargin" Value="4"/>
|
||||
</Style>
|
||||
|
||||
|
||||
|
||||
<Style x:Key="Placeholder" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Padding" Value="6,0,0,0" />
|
||||
<Setter Property="Foreground" Value="{x:Static SystemColors.GrayTextBrush}" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Properties" TargetType="{x:Type DataGrid}">
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
|
||||
@ -83,10 +91,11 @@
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="Payload" TargetType="{x:Type TextBox}">
|
||||
<Setter Property="AcceptsReturn" Value="True" />
|
||||
<Setter Property="AcceptsTab" Value="True" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Visible" />
|
||||
<Style x:Key="Payload" TargetType="{x:Type avalonedit:TextEditor}">
|
||||
<Setter Property="FontFamily" Value="Consolas,Courier New" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ControlBorder" TargetType="{x:Type Border}">
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
@ -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
|
||||
{
|
40
PettingZoo.WPF/ViewModel/DelegateCommand.cs
Normal file
40
PettingZoo.WPF/ViewModel/DelegateCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
79
PettingZoo.WPF/ViewModel/ObservableCollectionEx.cs
Normal file
79
PettingZoo.WPF/ViewModel/ObservableCollectionEx.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "Pett
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Tapeti", "PettingZoo.Tapeti\PettingZoo.Tapeti.csproj", "{1763AB04-59D9-4663-B207-D6302FFAACD5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.WPF", "PettingZoo.WPF\PettingZoo.WPF.csproj", "{E6617B69-2AC4-4056-B801-DD32E2374B71}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Test", "PettingZoo.Test\PettingZoo.Test.csproj", "{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -44,6 +48,14 @@ Global
|
||||
{7157B09C-FDD9-4928-B14D-C25B784CA865}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7157B09C-FDD9-4928-B14D-C25B784CA865}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6617B69-2AC4-4056-B801-DD32E2374B71}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
18
PettingZoo/Images/Example.svg
Normal file
18
PettingZoo/Images/Example.svg
Normal 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 |
9
PettingZoo/Images/Folder.svg
Normal file
9
PettingZoo/Images/Folder.svg
Normal 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 |
@ -19,9 +19,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Images\Connecting.svg" />
|
||||
<None Remove="Images\Busy.svg" />
|
||||
<None Remove="Images\Dock.svg" />
|
||||
<None Remove="Images\Error.svg" />
|
||||
<None Remove="Images\Example.svg" />
|
||||
<None Remove="Images\Folder.svg" />
|
||||
<None Remove="Images\Ok.svg" />
|
||||
<None Remove="Images\PublishSend.svg" />
|
||||
<None Remove="Images\Undock.svg" />
|
||||
@ -33,6 +35,8 @@
|
||||
<Resource Include="Images\Disconnect.svg" />
|
||||
<Resource Include="Images\Dock.svg" />
|
||||
<Resource Include="Images\Error.svg" />
|
||||
<Resource Include="Images\Example.svg" />
|
||||
<Resource Include="Images\Folder.svg" />
|
||||
<Resource Include="Images\Ok.svg" />
|
||||
<Resource Include="Images\Publish.svg" />
|
||||
<Resource Include="Images\PublishSend.svg" />
|
||||
@ -40,7 +44,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AvalonEdit" Version="6.1.3.50" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="SharpVectors" Version="1.7.7" />
|
||||
<PackageReference Include="SimpleInjector" Version="5.3.2" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
@ -50,11 +57,13 @@
|
||||
<ProjectReference Include="..\PettingZoo.Core\PettingZoo.Core.csproj" />
|
||||
<ProjectReference Include="..\PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj" />
|
||||
<ProjectReference Include="..\PettingZoo.Tapeti\PettingZoo.Tapeti.csproj" />
|
||||
<ProjectReference Include="..\PettingZoo.WPF\PettingZoo.WPF.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Images\Undock.svg" />
|
||||
<Resource Include="Images\Connecting.svg" />
|
||||
<Resource Include="Images\Busy.svg" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -68,11 +77,6 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
<Compile Update="UI\Example\ExamplePickerDialogStrings.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>ExamplePickerDialogStrings.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\Main\MainWindowStrings.Designer.cs">
|
||||
<DependentUpon>MainWindowStrings.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
@ -127,10 +131,6 @@
|
||||
<LastGenOutput>ConnectionWindowStrings.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="UI\Example\ExamplePickerDialogStrings.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>ExamplePickerDialogStrings.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="UI\Main\MainWindowStrings.resx">
|
||||
<LastGenOutput>MainWindowStrings.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
|
@ -1,15 +1,18 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
using PettingZoo.Core.Connection;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.Core.Settings;
|
||||
using PettingZoo.RabbitMQ;
|
||||
using PettingZoo.Settings.LiteDB;
|
||||
using PettingZoo.Tapeti;
|
||||
using PettingZoo.UI.Connection;
|
||||
using PettingZoo.UI.Main;
|
||||
using PettingZoo.UI.Subscribe;
|
||||
using Serilog;
|
||||
using SimpleInjector;
|
||||
|
||||
namespace PettingZoo
|
||||
@ -23,23 +26,59 @@ namespace PettingZoo
|
||||
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(
|
||||
XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
|
||||
|
||||
var container = Bootstrap();
|
||||
RunApplication(container);
|
||||
try
|
||||
{
|
||||
var logger = CreateLogger();
|
||||
try
|
||||
{
|
||||
logger.Verbose("Bootstrapping...");
|
||||
var container = Bootstrap(logger);
|
||||
|
||||
logger.Verbose("Running application...");
|
||||
RunApplication(container, logger);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Unhandled exception");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
logger.Verbose("Shutting down");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_ = MessageBox.Show($"Unhandled exception: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Container Bootstrap()
|
||||
private static ILogger CreateLogger()
|
||||
{
|
||||
var logPath = Path.Combine(PettingZooPaths.LogPath, @"PettingZoo.log");
|
||||
|
||||
return new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.WriteTo.File(logPath, rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
|
||||
private static Container Bootstrap(ILogger logger)
|
||||
{
|
||||
var container = new Container();
|
||||
|
||||
// See comments in RunApplication
|
||||
container.Options.EnableAutoVerification = false;
|
||||
|
||||
container.RegisterInstance(logger);
|
||||
container.Register<IConnectionFactory, RabbitMQClientConnectionFactory>();
|
||||
container.Register<IConnectionDialog, WindowConnectionDialog>();
|
||||
container.Register<ISubscribeDialog, WindowSubscribeDialog>();
|
||||
container.Register<IConnectionSettingsRepository, LiteDBConnectionSettingsRepository>();
|
||||
container.Register<IUISettingsRepository, LiteDBUISettingsRepository>();
|
||||
container.Register<IExampleGenerator, TapetiClassLibraryExampleGenerator>();
|
||||
|
||||
container.Register<MainWindow>();
|
||||
|
||||
@ -47,32 +86,25 @@ namespace PettingZoo
|
||||
}
|
||||
|
||||
|
||||
private static void RunApplication(Container container)
|
||||
private static void RunApplication(Container container, ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var app = new App(container);
|
||||
app.InitializeComponent();
|
||||
var app = new App(container, logger);
|
||||
app.InitializeComponent();
|
||||
|
||||
#if DEBUG
|
||||
// Verify container after initialization to prevent issues loading the resource dictionaries
|
||||
container.Verify();
|
||||
#if DEBUG
|
||||
// Verify container after initialization to prevent issues loading the resource dictionaries
|
||||
container.Verify();
|
||||
|
||||
// This causes the MainWindow and Windows properties to be populated however, which we don't want
|
||||
// because then the app does not close properly when using OnMainWindowClose, so clean up the mess
|
||||
app.MainWindow = null;
|
||||
foreach (var window in app.Windows)
|
||||
((Window)window).Close();
|
||||
|
||||
// All this is the reason we only perform verification in debug builds
|
||||
#endif
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MessageBox.Show($"Fatal exception: {e.Message}", @"PettingZoo", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
// This causes the MainWindow and Windows properties to be populated however, which we don't want
|
||||
// because then the app does not close properly when using OnMainWindowClose, so clean up the mess
|
||||
app.MainWindow = null;
|
||||
foreach (var window in app.Windows)
|
||||
((Window)window).Close();
|
||||
|
||||
// All this is the reason we only perform verification in debug builds
|
||||
#endif
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
Must-have
|
||||
---------
|
||||
- Check required fields before enabling Publish button
|
||||
|
||||
|
||||
Should-have
|
||||
-----------
|
||||
- Save / load publisher messages (either as templates or to disk)
|
||||
- Export received messages to Tapeti JSON file / Tapeti.Cmd command-line
|
||||
- Tapeti: export received messages to Tapeti.Cmd JSON file / Tapeti.Cmd command-line
|
||||
- Tapeti: fetch NuGet dependencies to improve the chances of succesfully loading the assembly, instead of the current "extraAssembliesPaths" workaround
|
||||
|
||||
|
||||
Nice-to-have
|
||||
------------
|
||||
- JSON syntax highlighting
|
||||
- Validation against message classes (for Tapeti messages)
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
|
||||
namespace PettingZoo.UI.Connection
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using PettingZoo.Core.Settings;
|
||||
|
||||
namespace PettingZoo.UI.Connection
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
PettingZoo/UI/ErrorHighlightingTransformer.cs
Normal file
37
PettingZoo/UI/ErrorHighlightingTransformer.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
using ICSharpCode.AvalonEdit.Document;
|
||||
using ICSharpCode.AvalonEdit.Rendering;
|
||||
|
||||
namespace PettingZoo.UI
|
||||
{
|
||||
public class ErrorHighlightingTransformer : DocumentColorizingTransformer
|
||||
{
|
||||
public Brush BackgroundBrush { get; set; }
|
||||
public TextPosition? ErrorPosition { get; set; }
|
||||
|
||||
|
||||
public ErrorHighlightingTransformer()
|
||||
{
|
||||
BackgroundBrush = new SolidColorBrush(Color.FromRgb(255, 230, 230));
|
||||
}
|
||||
|
||||
|
||||
protected override void ColorizeLine(DocumentLine line)
|
||||
{
|
||||
if (ErrorPosition == null)
|
||||
return;
|
||||
|
||||
if (line.LineNumber != Math.Max(ErrorPosition.Value.Row, 1))
|
||||
return;
|
||||
|
||||
var lineStartOffset = line.Offset;
|
||||
|
||||
ChangeLinePart(lineStartOffset, lineStartOffset + line.Length,
|
||||
element =>
|
||||
{
|
||||
element.BackgroundBrush = BackgroundBrush;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace PettingZoo.UI.Example
|
||||
{
|
||||
public class ExamplePickerDialogViewModel
|
||||
{
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
// ReSharper disable UnusedMember.Global - public API
|
||||
|
||||
namespace PettingZoo.UI
|
||||
{
|
||||
// Source: https://social.msdn.microsoft.com/Forums/vstudio/en-US/0f524459-b14e-4f9a-8264-267953418a2d/trivial-listboxlistview-autoscroll?forum=wpf
|
||||
@ -69,6 +71,8 @@ namespace PettingZoo.UI
|
||||
public void Dispose()
|
||||
{
|
||||
BindingOperations.ClearBinding(this, ItemsSourceProperty);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
||||
|
@ -79,7 +79,7 @@
|
||||
<StatusBar DockPanel.Dock="Bottom">
|
||||
<StatusBarItem>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Connecting.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusConnecting}" />
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Busy.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusConnecting}" />
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusOk}" />
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusError}" />
|
||||
<TextBlock Text="{Binding ConnectionStatus}" VerticalAlignment="Center"/>
|
||||
|
@ -5,6 +5,7 @@ using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using PettingZoo.Core.Connection;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.UI.Connection;
|
||||
using PettingZoo.UI.Subscribe;
|
||||
using PettingZoo.UI.Tab;
|
||||
@ -19,11 +20,15 @@ namespace PettingZoo.UI.Main
|
||||
public bool WasMaximized;
|
||||
|
||||
|
||||
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog)
|
||||
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, IExampleGenerator exampleGenerator)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
||||
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this);
|
||||
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, this, exampleGenerator)
|
||||
{
|
||||
TabHostWindow = this
|
||||
};
|
||||
|
||||
DataContext = viewModel;
|
||||
InitializeComponent();
|
||||
|
||||
@ -115,6 +120,8 @@ namespace PettingZoo.UI.Main
|
||||
private static T? GetParent<T>(object originalSource) where T : DependencyObject
|
||||
{
|
||||
var current = originalSource as DependencyObject;
|
||||
if (current is not Visual)
|
||||
return null;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
|
@ -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!)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace PettingZoo.UI.Subscribe
|
||||
namespace PettingZoo.UI.Subscribe
|
||||
{
|
||||
public interface ISubscribeDialog
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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; }
|
||||
|
@ -17,15 +17,23 @@
|
||||
<RadioButton Content="JSON" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Json}}" />
|
||||
<RadioButton Content="Plain text" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Plain}}" />
|
||||
<RadioButton Content="Other" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Other}}" />
|
||||
<TextBox Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox Name="TextBoxForBorder" Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding JsonValidationVisibility}" Margin="0,8,0,0">
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationOk}" />
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationError}" />
|
||||
<TextBlock Text="{Binding JsonValidationMessage}" Margin="4" />
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding ValidationVisibility}" Margin="0,8,0,0">
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationOk}" />
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationError}" />
|
||||
<Image Source="{svgc:SvgImage Source=/Images/Busy.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationValidating}" />
|
||||
<TextBlock Text="{Binding ValidationMessage}" Margin="4" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBox Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource Payload}" />
|
||||
<Border Style="{StaticResource ControlBorder}" Name="EditorBorder">
|
||||
<avalonedit:TextEditor
|
||||
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
|
||||
Name="Editor"
|
||||
SyntaxHighlighting="{Binding SyntaxHighlighting}"
|
||||
Style="{StaticResource Payload}"
|
||||
/>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
|
@ -86,6 +86,9 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new();
|
||||
|
||||
public PayloadEditorControl()
|
||||
{
|
||||
// Keep the exposed properties in sync with the ViewModel
|
||||
@ -133,6 +136,63 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// I'm not sure how to get a standard control border, all I could find were workaround:
|
||||
// https://social.msdn.microsoft.com/Forums/en-US/5e007497-8d5a-401d-ac5b-9e1356fe9b64/default-borderbrush-for-textbox-listbox-etc
|
||||
// So I'll just copy it from another TextBox. I truly hate WPF some times for making standard things so complicated. </rant>
|
||||
EditorBorder.BorderBrush = TextBoxForBorder.BorderBrush;
|
||||
|
||||
Editor.Options.IndentationSize = 2;
|
||||
Editor.TextArea.TextView.LineTransformers.Add(errorHighlightingTransformer);
|
||||
|
||||
// Avalon doesn't play nice with bindings it seems:
|
||||
// https://stackoverflow.com/questions/18964176/two-way-binding-to-avalonedit-document-text-using-mvvm
|
||||
Editor.Document.Text = Payload;
|
||||
|
||||
|
||||
var editorTriggered = false;
|
||||
|
||||
Editor.TextChanged += (_, _) =>
|
||||
{
|
||||
editorTriggered = true;
|
||||
try
|
||||
{
|
||||
Payload = Editor.Document.Text;
|
||||
}
|
||||
finally
|
||||
{
|
||||
editorTriggered = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
viewModel.PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName != nameof(viewModel.ValidationInfo) &&
|
||||
(args.PropertyName != nameof(viewModel.Payload) || editorTriggered))
|
||||
return;
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
switch (args.PropertyName)
|
||||
{
|
||||
case nameof(viewModel.ValidationInfo):
|
||||
if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
|
||||
return;
|
||||
|
||||
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
|
||||
|
||||
// TODO this can probably be optimized to only redraw the affected line
|
||||
Editor.TextArea.TextView.Redraw();
|
||||
break;
|
||||
|
||||
case nameof(viewModel.Payload):
|
||||
Editor.Document.Text = viewModel.Payload;
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Setting the DataContext for the UserControl is a major PITA when binding the control's properties,
|
||||
// so I've moved the ViewModel one level down to get the best of both worlds...
|
||||
DataContextContainer.DataContext = viewModel;
|
||||
|
@ -88,20 +88,29 @@ namespace PettingZoo.UI.Tab.Publisher {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Invalid JSON: {0}.
|
||||
/// Looks up a localized string similar to Invalid: {0}.
|
||||
/// </summary>
|
||||
internal static string JsonValidationError {
|
||||
internal static string ValidationError {
|
||||
get {
|
||||
return ResourceManager.GetString("JsonValidationError", resourceCulture);
|
||||
return ResourceManager.GetString("ValidationError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Valid JSON.
|
||||
/// Looks up a localized string similar to Valid.
|
||||
/// </summary>
|
||||
internal static string JsonValidationOk {
|
||||
internal static string ValidationOk {
|
||||
get {
|
||||
return ResourceManager.GetString("JsonValidationOk", resourceCulture);
|
||||
return ResourceManager.GetString("ValidationOk", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Validating....
|
||||
/// </summary>
|
||||
internal static string ValidationValidating {
|
||||
get {
|
||||
return ResourceManager.GetString("ValidationValidating", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,10 +112,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ContentTypeJson" xml:space="preserve">
|
||||
<value>JSON</value>
|
||||
@ -126,10 +126,13 @@
|
||||
<data name="ContentTypePlain" xml:space="preserve">
|
||||
<value>Plain text</value>
|
||||
</data>
|
||||
<data name="JsonValidationError" xml:space="preserve">
|
||||
<value>Invalid JSON: {0}</value>
|
||||
<data name="ValidationError" xml:space="preserve">
|
||||
<value>Invalid: {0}</value>
|
||||
</data>
|
||||
<data name="JsonValidationOk" xml:space="preserve">
|
||||
<value>Valid JSON</value>
|
||||
<data name="ValidationOk" xml:space="preserve">
|
||||
<value>Valid</value>
|
||||
</data>
|
||||
<data name="ValidationValidating" xml:space="preserve">
|
||||
<value>Validating...</value>
|
||||
</data>
|
||||
</root>
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Windows;
|
||||
using ICSharpCode.AvalonEdit.Highlighting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
|
||||
namespace PettingZoo.UI.Tab.Publisher
|
||||
{
|
||||
@ -12,7 +14,39 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
Json,
|
||||
Plain,
|
||||
Other
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public enum ValidationStatus
|
||||
{
|
||||
NotSupported,
|
||||
Validating,
|
||||
Ok,
|
||||
Error
|
||||
}
|
||||
|
||||
|
||||
public readonly struct ValidationInfo
|
||||
{
|
||||
public ValidationStatus Status { get; }
|
||||
public string Message { get; }
|
||||
public TextPosition? ErrorPosition { get; }
|
||||
|
||||
|
||||
public ValidationInfo(ValidationStatus status, string? message = null, TextPosition? errorPosition = null)
|
||||
{
|
||||
Status = status;
|
||||
Message = message ?? status switch
|
||||
{
|
||||
ValidationStatus.NotSupported => "",
|
||||
ValidationStatus.Validating => PayloadEditorStrings.ValidationValidating,
|
||||
ValidationStatus.Ok => PayloadEditorStrings.ValidationOk,
|
||||
ValidationStatus.Error => throw new InvalidOperationException(@"Message required for Error validation status"),
|
||||
_ => throw new ArgumentException(@"Unsupported validation status", nameof(status))
|
||||
};
|
||||
ErrorPosition = errorPosition;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PayloadEditorViewModel : BaseViewModel
|
||||
@ -24,8 +58,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json;
|
||||
private bool fixedJson;
|
||||
|
||||
private bool jsonValid = true;
|
||||
private string jsonValidationMessage;
|
||||
private ValidationInfo validationInfo = new(ValidationStatus.Ok);
|
||||
|
||||
private string payload = "";
|
||||
|
||||
@ -59,7 +92,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
get => contentTypeSelection;
|
||||
set
|
||||
{
|
||||
if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(JsonValidationVisibility) }))
|
||||
if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(ValidationVisibility), nameof(SyntaxHighlighting) }))
|
||||
return;
|
||||
|
||||
ContentType = ContentTypeSelection switch
|
||||
@ -80,23 +113,22 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
set => SetField(ref fixedJson, value);
|
||||
}
|
||||
|
||||
public Visibility JsonValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility JsonValidationOk => JsonValid ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility JsonValidationError => !JsonValid ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
|
||||
public string JsonValidationMessage
|
||||
public ValidationInfo ValidationInfo
|
||||
{
|
||||
get => jsonValidationMessage;
|
||||
private set => SetField(ref jsonValidationMessage, value);
|
||||
get => validationInfo;
|
||||
private set => SetField(ref validationInfo, value, otherPropertiesChanged: new[] { nameof(ValidationOk), nameof(ValidationError), nameof(ValidationValidating), nameof(ValidationMessage) });
|
||||
}
|
||||
|
||||
|
||||
public bool JsonValid
|
||||
{
|
||||
get => jsonValid;
|
||||
private set => SetField(ref jsonValid, value, otherPropertiesChanged: new[] { nameof(JsonValidationOk), nameof(JsonValidationError) });
|
||||
}
|
||||
public Visibility ValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public string ValidationMessage => ValidationInfo.Message;
|
||||
|
||||
public Visibility ValidationOk => ValidationInfo.Status == ValidationStatus.Ok ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility ValidationError => ValidationInfo.Status == ValidationStatus.Error ? Visibility.Visible : Visibility.Collapsed;
|
||||
public Visibility ValidationValidating => ValidationInfo.Status == ValidationStatus.Validating ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
|
||||
public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
@ -108,26 +140,48 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
}
|
||||
|
||||
|
||||
public IHighlightingDefinition? SyntaxHighlighting => ContentTypeSelection == PayloadEditorContentType.Json
|
||||
? HighlightingManager.Instance.GetDefinition(@"Json")
|
||||
: null;
|
||||
|
||||
|
||||
|
||||
public PayloadEditorViewModel()
|
||||
{
|
||||
jsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
|
||||
|
||||
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
|
||||
var observable = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
|
||||
h => PropertyChanged += h,
|
||||
h => PropertyChanged -= h)
|
||||
.Where(e => e.EventArgs.PropertyName == nameof(Payload))
|
||||
.Where(e => e.EventArgs.PropertyName == nameof(Payload));
|
||||
|
||||
observable
|
||||
.Subscribe(_ => ValidatingPayload());
|
||||
|
||||
observable
|
||||
.Throttle(TimeSpan.FromMilliseconds(500))
|
||||
.Subscribe(_ => ValidatePayload());
|
||||
}
|
||||
|
||||
|
||||
private void ValidatingPayload()
|
||||
{
|
||||
if (ValidationInfo.Status == ValidationStatus.Validating)
|
||||
return;
|
||||
|
||||
if (ContentTypeSelection != PayloadEditorContentType.Json)
|
||||
{
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
|
||||
return;
|
||||
}
|
||||
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Validating);
|
||||
}
|
||||
|
||||
|
||||
private void ValidatePayload()
|
||||
{
|
||||
if (ContentTypeSelection != PayloadEditorContentType.Json)
|
||||
{
|
||||
JsonValid = true;
|
||||
JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -136,13 +190,15 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
if (!string.IsNullOrEmpty(Payload))
|
||||
JToken.Parse(Payload);
|
||||
|
||||
JsonValid = true;
|
||||
JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Ok);
|
||||
}
|
||||
catch (JsonReaderException e)
|
||||
{
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message, new TextPosition(e.LineNumber, e.LinePosition));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
JsonValid = false;
|
||||
JsonValidationMessage = string.Format(PayloadEditorStrings.JsonValidationError, e.Message);
|
||||
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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!)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -1,21 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using PettingZoo.Core.Connection;
|
||||
using PettingZoo.Core.Generator;
|
||||
using PettingZoo.WPF.ViewModel;
|
||||
using IConnection = PettingZoo.Core.Connection.IConnection;
|
||||
|
||||
namespace PettingZoo.UI.Tab.Publisher
|
||||
{
|
||||
public class TapetiPublisherViewModel : BaseViewModel
|
||||
public class TapetiPublisherViewModel : BaseViewModel, ITabHostWindowNotify
|
||||
{
|
||||
private readonly IConnection connection;
|
||||
private readonly IPublishDestination publishDestination;
|
||||
private readonly IExampleGenerator exampleGenerator;
|
||||
private readonly DelegateCommand publishCommand;
|
||||
private readonly DelegateCommand browseClassCommand;
|
||||
|
||||
private string correlationId = "";
|
||||
private string payload = "";
|
||||
private string className = "";
|
||||
private string assemblyName = "";
|
||||
private Window? tabHostWindow;
|
||||
|
||||
|
||||
public string CorrelationId
|
||||
@ -51,6 +58,7 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
|
||||
|
||||
public ICommand PublishCommand => publishCommand;
|
||||
public ICommand BrowseClassCommand => browseClassCommand;
|
||||
|
||||
|
||||
|
||||
@ -81,12 +89,14 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
}
|
||||
|
||||
|
||||
public TapetiPublisherViewModel(IConnection connection, IPublishDestination publishDestination, ReceivedMessageInfo? receivedMessage = null)
|
||||
public TapetiPublisherViewModel(IConnection connection, IPublishDestination publishDestination, IExampleGenerator exampleGenerator, ReceivedMessageInfo? receivedMessage = null)
|
||||
{
|
||||
this.connection = connection;
|
||||
this.publishDestination = publishDestination;
|
||||
this.exampleGenerator = exampleGenerator;
|
||||
|
||||
publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
|
||||
browseClassCommand = new DelegateCommand(BrowseClassExecute);
|
||||
|
||||
|
||||
if (receivedMessage == null || !IsTapetiMessage(receivedMessage, out var receivedAssemblyName, out var receivedClassName))
|
||||
@ -99,6 +109,29 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
}
|
||||
|
||||
|
||||
private void BrowseClassExecute()
|
||||
{
|
||||
exampleGenerator.Select(tabHostWindow, example =>
|
||||
{
|
||||
Dispatcher.CurrentDispatcher.BeginInvoke(() =>
|
||||
{
|
||||
switch (example)
|
||||
{
|
||||
case null:
|
||||
return;
|
||||
|
||||
case IClassTypeExample classTypeExample:
|
||||
AssemblyName = classTypeExample.AssemblyName;
|
||||
ClassName = classTypeExample.FullClassName;
|
||||
break;
|
||||
}
|
||||
|
||||
Payload = example.Generate();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void PublishExecute()
|
||||
{
|
||||
static string? NullIfEmpty(string? value)
|
||||
@ -125,17 +158,22 @@ namespace PettingZoo.UI.Tab.Publisher
|
||||
}
|
||||
|
||||
|
||||
private bool PublishCanExecute()
|
||||
private static bool PublishCanExecute()
|
||||
{
|
||||
// TODO validate input
|
||||
return true;
|
||||
}
|
||||
|
||||
public void HostWindowChanged(Window? hostWindow)
|
||||
{
|
||||
tabHostWindow = hostWindow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class DesignTimeTapetiPublisherViewModel : TapetiPublisherViewModel
|
||||
{
|
||||
public DesignTimeTapetiPublisherViewModel() : base(null!, null!)
|
||||
public DesignTimeTapetiPublisherViewModel() : base(null!, null!, null!)
|
||||
{
|
||||
AssemblyName = "Messaging.Example";
|
||||
ClassName = "Messaging.Example.ExampleMessage";
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
43
PettingZoo/UI/TextPosition.cs
Normal file
43
PettingZoo/UI/TextPosition.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
namespace PettingZoo.UI
|
||||
{
|
||||
public readonly struct TextPosition : IEquatable<TextPosition>
|
||||
{
|
||||
public int Row { get; }
|
||||
public int Column { get; }
|
||||
|
||||
|
||||
public TextPosition(int row, int column)
|
||||
{
|
||||
Row = row;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
|
||||
public bool Equals(TextPosition other)
|
||||
{
|
||||
return Row == other.Row && Column == other.Column;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is TextPosition other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Row, Column);
|
||||
}
|
||||
|
||||
public static bool operator ==(TextPosition left, TextPosition right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(TextPosition left, TextPosition right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
# ![Petting Zoo](https://raw.githubusercontent.com/PsychoMark/PettingZoo/master/Images/PettingZoo-48.png) Petting Zoo
|
||||
# ![Petting Zoo](https://raw.githubusercontent.com/MvRens/PettingZoo/master/PettingZoo/Images/PettingZoo-48.png) Petting Zoo
|
||||
##### A RabbitMQ live message viewer
|
||||
|
||||
ToDo: explain how it brings you coffee, fame and world peace. Or maybe just makes watching the messages flow slightly more comfortable.
|
||||
|
Loading…
Reference in New Issue
Block a user