Add project files.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record ConfigurationChanged<TConfiguration>(TConfiguration Configuration) where TConfiguration : class;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class ConfigurationInitializer<TConfiguration> : IInitializer where TConfiguration : class, new()
|
||||
{
|
||||
private readonly TConfiguration configuration;
|
||||
private readonly IEventAggregator eventAggregator;
|
||||
|
||||
public ConfigurationInitializer(TConfiguration configuration, IEventAggregator eventAggregator)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
eventAggregator.Publish(configuration);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class ConfigurationWriter<TConfiguration> : IConfigurationWriter<TConfiguration> where TConfiguration : class, new()
|
||||
{
|
||||
private readonly IConfiguration rootConfiguration;
|
||||
|
||||
public ConfigurationWriter(IConfiguration rootConfiguration)
|
||||
{
|
||||
this.rootConfiguration = rootConfiguration;
|
||||
}
|
||||
|
||||
public void Write(string section, TConfiguration configuration)
|
||||
{
|
||||
if (rootConfiguration is IConfigurationRoot root)
|
||||
{
|
||||
foreach (IConfigurationProvider? provider in root.Providers)
|
||||
{
|
||||
if (provider is IWritableConfigurationProvider writableConfigurationProvider)
|
||||
{
|
||||
writableConfigurationProvider.Write(section, configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IConfigurationWriter<TConfiguration> where TConfiguration : class
|
||||
{
|
||||
void Write(string section, TConfiguration args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IWritableConfigurationProvider
|
||||
{
|
||||
void Write<TValue>(string section, TValue value) where TValue : class, new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IWritableJsonConfigurationDescriptor
|
||||
{
|
||||
Type ConfigurationType { get; }
|
||||
|
||||
string Key { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.IO;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IWritableJsonConfigurationBuilder
|
||||
{
|
||||
Stream? DefaultFileStream { get; }
|
||||
|
||||
IWritableJsonConfigurationBuilder AddDefaultConfiguration<TConfiguration>(string Key) where TConfiguration : class;
|
||||
|
||||
IWritableJsonConfigurationBuilder AddDefaultFileStream(Stream stream);
|
||||
|
||||
void Build(string path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Json.Patch;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class WritableJsonConfigurationBuilder : IWritableJsonConfigurationBuilder
|
||||
{
|
||||
private readonly List<IWritableJsonConfigurationDescriptor> descriptors = new();
|
||||
|
||||
public Stream? DefaultFileStream { get; private set; }
|
||||
|
||||
public IReadOnlyCollection<IWritableJsonConfigurationDescriptor> Descriptors => new ReadOnlyCollection<IWritableJsonConfigurationDescriptor>(descriptors);
|
||||
|
||||
public IWritableJsonConfigurationBuilder AddDefaultConfiguration<TConfiguration>(string Key) where TConfiguration : class
|
||||
{
|
||||
descriptors.Add(new WritableJsonConfigurationDescriptor(typeof(TConfiguration), Key));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWritableJsonConfigurationBuilder AddDefaultFileStream(Stream stream)
|
||||
{
|
||||
DefaultFileStream = stream;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Build(string path)
|
||||
{
|
||||
JObject? sourceDocument = new();
|
||||
if (TryLoadSource(out string? defaultContent))
|
||||
{
|
||||
sourceDocument = JObject.Parse(defaultContent!);
|
||||
}
|
||||
|
||||
JObject? targetDocument = new();
|
||||
if (TryLoadTarget(path, out string? targetContent))
|
||||
{
|
||||
targetDocument = JObject.Parse(targetContent!);
|
||||
}
|
||||
|
||||
foreach (IWritableJsonConfigurationDescriptor? descriptor in descriptors)
|
||||
{
|
||||
if (sourceDocument.SelectToken($"$.{descriptor.Key}") is JToken sourceSection)
|
||||
{
|
||||
if (targetDocument.SelectToken($"$.{descriptor.Key}") is JToken targetSection)
|
||||
{
|
||||
object? source = JsonSerializer.Deserialize(JsonConvert.SerializeObject(sourceSection), descriptor.ConfigurationType);
|
||||
object? target = JsonSerializer.Deserialize(JsonConvert.SerializeObject(targetSection), descriptor.ConfigurationType);
|
||||
|
||||
JsonPatch? patch = source.CreatePatch(target);
|
||||
|
||||
object? sourcePatched = patch.Apply(source);
|
||||
targetSection.Replace(JToken.Parse(JsonSerializer.Serialize(sourcePatched, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })));
|
||||
}
|
||||
else
|
||||
{
|
||||
object? source = JsonSerializer.Deserialize(JsonConvert.SerializeObject(sourceSection), descriptor.ConfigurationType);
|
||||
targetDocument.Add(descriptor.Key, JToken.Parse(JsonSerializer.Serialize(source, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
object? configuration = Activator.CreateInstance(descriptor.ConfigurationType);
|
||||
targetDocument.Add(descriptor.Key, JToken.Parse(JsonSerializer.Serialize(configuration, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })));
|
||||
}
|
||||
}
|
||||
|
||||
using FileStream? fileStream = new(path, FileMode.Create, FileAccess.Write);
|
||||
using StreamWriter streamWriter = new(fileStream);
|
||||
using JsonTextWriter writer = new(streamWriter) { Formatting = Formatting.Indented };
|
||||
targetDocument.WriteTo(writer);
|
||||
}
|
||||
|
||||
private bool TryLoadTarget(string path, [MaybeNull] out string? content)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
using FileStream? fileStream = new(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using StreamReader? streamReader = new(fileStream);
|
||||
content = streamReader.ReadToEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
content = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryLoadSource([MaybeNull] out string? content)
|
||||
{
|
||||
if (DefaultFileStream is Stream fileStream)
|
||||
{
|
||||
using StreamReader? streamReader = new(fileStream);
|
||||
content = streamReader.ReadToEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
content = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record WritableJsonConfigurationDescriptor(Type ConfigurationType, string Key) : IWritableJsonConfigurationDescriptor;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class WritableJsonConfigurationExtensions
|
||||
{
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
string path)
|
||||
{
|
||||
return builder.AddWritableJsonFile(null, path, false, false, null);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
string path,
|
||||
Action<IWritableJsonConfigurationBuilder>? factoryDelegate)
|
||||
{
|
||||
return builder.AddWritableJsonFile(null, path, false, false, factoryDelegate);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
string path,
|
||||
bool optional)
|
||||
{
|
||||
return builder.AddWritableJsonFile(null, path, optional, false, null);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
string path,
|
||||
bool optional,
|
||||
Action<IWritableJsonConfigurationBuilder>? factoryDelegate)
|
||||
{
|
||||
return builder.AddWritableJsonFile(null, path, optional, false, factoryDelegate);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
string path,
|
||||
bool optional,
|
||||
bool reloadOnChange)
|
||||
{
|
||||
return builder.AddWritableJsonFile(null, path, optional, reloadOnChange, null);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
string path,
|
||||
bool optional,
|
||||
bool reloadOnChange,
|
||||
Action<IWritableJsonConfigurationBuilder>? factoryDelegate)
|
||||
{
|
||||
return builder.AddWritableJsonFile(null, path, optional, reloadOnChange, factoryDelegate);
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder,
|
||||
IFileProvider? provider,
|
||||
string path,
|
||||
bool optional,
|
||||
bool reloadOnChange, Action<IWritableJsonConfigurationBuilder>? writableJsonConfigurationDelegate)
|
||||
{
|
||||
IWritableJsonConfigurationBuilder writableJsonConfigurationBuilder = new WritableJsonConfigurationBuilder();
|
||||
writableJsonConfigurationDelegate?.Invoke(writableJsonConfigurationBuilder);
|
||||
|
||||
return builder.AddWritableJsonFile(configuration =>
|
||||
{
|
||||
configuration.FileProvider = provider;
|
||||
configuration.Path = path;
|
||||
configuration.Optional = optional;
|
||||
configuration.ReloadOnChange = reloadOnChange;
|
||||
configuration.Factory = writableJsonConfigurationBuilder;
|
||||
configuration.ResolveFileProvider();
|
||||
});
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddWritableJsonFile(this IConfigurationBuilder builder, Action<WritableJsonConfigurationSource> configureSource) => builder.Add(configureSource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
internal class WritableJsonConfigurationFile
|
||||
{
|
||||
private readonly Dictionary<string, (JsonValueKind?, string?)> data = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Stack<string> paths = new();
|
||||
private JObject tokenCache = new();
|
||||
private bool isParsing;
|
||||
|
||||
public IDictionary<string, string?> Parse(Stream input)
|
||||
{
|
||||
return ParseStream(input);
|
||||
}
|
||||
|
||||
public void Write(string key, string value, Stream output)
|
||||
{
|
||||
if (isParsing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (key[^1] == ':')
|
||||
{
|
||||
key = key[0..^1];
|
||||
}
|
||||
|
||||
string? tokenPath = $"$.{key.Replace(":", ".")}";
|
||||
key = key.Replace("[", "").Replace("]", "");
|
||||
|
||||
if (tokenCache.SelectToken(tokenPath) is JToken token && data.ContainsKey(key))
|
||||
{
|
||||
(JsonValueKind? kind, string _) = data[key];
|
||||
object? newValue = ConvertValue(kind, value);
|
||||
|
||||
data[key] = new(kind, value);
|
||||
token.Replace(JToken.FromObject(newValue));
|
||||
}
|
||||
|
||||
using StreamWriter streamWriter = new(output);
|
||||
using JsonTextWriter writer = new(streamWriter) { Formatting = Formatting.Indented };
|
||||
tokenCache.WriteTo(writer);
|
||||
|
||||
output.SetLength(output.Position);
|
||||
}
|
||||
|
||||
private static object ConvertValue(JsonValueKind? kind, string value)
|
||||
{
|
||||
return kind is JsonValueKind.True or JsonValueKind.False ? bool.Parse(value) : value;
|
||||
}
|
||||
|
||||
private void EnterContext(string context)
|
||||
{
|
||||
paths.Push(paths.Count > 0 ? paths.Peek() + ConfigurationPath.KeyDelimiter + context : context);
|
||||
}
|
||||
|
||||
private void ExitContext()
|
||||
{
|
||||
paths.Pop();
|
||||
}
|
||||
|
||||
private IDictionary<string, string?> ParseStream(Stream input)
|
||||
{
|
||||
data.Clear();
|
||||
|
||||
JsonDocumentOptions jsonDocumentOptions = new()
|
||||
{
|
||||
CommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
|
||||
isParsing = true;
|
||||
using (StreamReader? reader = new(input))
|
||||
{
|
||||
string? content = reader.ReadToEnd();
|
||||
tokenCache = JObject.Parse(content);
|
||||
|
||||
using JsonDocument? doc = JsonDocument.Parse(content, jsonDocumentOptions);
|
||||
VisitElement(doc.RootElement);
|
||||
}
|
||||
isParsing = false;
|
||||
|
||||
return data.ToDictionary(k => k.Key, v => v.Value.Item2?.ToString() ?? null);
|
||||
}
|
||||
|
||||
private void VisitElement(JsonElement element)
|
||||
{
|
||||
bool isEmpty = true;
|
||||
|
||||
foreach (JsonProperty property in element.EnumerateObject())
|
||||
{
|
||||
isEmpty = false;
|
||||
|
||||
EnterContext(property.Name);
|
||||
VisitValue(property.Value);
|
||||
ExitContext();
|
||||
}
|
||||
|
||||
if (isEmpty && paths.Count > 0)
|
||||
{
|
||||
data[paths.Peek()] = (JsonValueKind.Null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitValue(JsonElement value)
|
||||
{
|
||||
switch (value.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
VisitElement(value);
|
||||
break;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
int index = 0;
|
||||
foreach (JsonElement arrayElement in value.EnumerateArray())
|
||||
{
|
||||
EnterContext(index.ToString());
|
||||
VisitValue(arrayElement);
|
||||
ExitContext();
|
||||
index++;
|
||||
}
|
||||
break;
|
||||
|
||||
case JsonValueKind.Number:
|
||||
case JsonValueKind.String:
|
||||
case JsonValueKind.True:
|
||||
case JsonValueKind.False:
|
||||
case JsonValueKind.Null:
|
||||
string key = paths.Peek();
|
||||
if (data.ContainsKey(key))
|
||||
{
|
||||
throw new FormatException();
|
||||
}
|
||||
data[key] = new(value.ValueKind, value.ToString());
|
||||
break;
|
||||
|
||||
case JsonValueKind.Undefined:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.Extensions.Configuration.Json;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.IO;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class WritableJsonConfigurationProvider : JsonConfigurationProvider, IWritableConfigurationProvider
|
||||
{
|
||||
public WritableJsonConfigurationProvider(JsonConfigurationSource source) : base(source)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Write<TValue>(string section, TValue value) where TValue : class, new()
|
||||
{
|
||||
IFileInfo? file = Source.FileProvider?.GetFileInfo(Source.Path ?? string.Empty);
|
||||
static Stream OpenRead(IFileInfo fileInfo)
|
||||
{
|
||||
return fileInfo.PhysicalPath is not null
|
||||
? new FileStream(fileInfo.PhysicalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)
|
||||
: fileInfo.CreateReadStream();
|
||||
}
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
using Stream stream = OpenRead(file);
|
||||
using StreamReader? reader = new(stream);
|
||||
|
||||
string? content = reader.ReadToEnd();
|
||||
JObject? document = JObject.Parse(content);
|
||||
|
||||
if (document.SelectToken($"$.{section}") is JToken sectionToken)
|
||||
{
|
||||
sectionToken.Replace(JToken.Parse(JsonConvert.SerializeObject(value)));
|
||||
stream.SetLength(0);
|
||||
|
||||
using StreamWriter streamWriter = new(stream);
|
||||
using JsonTextWriter writer = new(streamWriter) { Formatting = Formatting.Indented };
|
||||
document.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Json;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class WritableJsonConfigurationSource : JsonConfigurationSource
|
||||
{
|
||||
public IWritableJsonConfigurationBuilder? Factory { get; set; }
|
||||
|
||||
public override IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
EnsureDefaultsWithSteam(builder);
|
||||
return new WritableJsonConfigurationProvider(this);
|
||||
}
|
||||
|
||||
private void EnsureDefaultsWithSteam(IConfigurationBuilder builder)
|
||||
{
|
||||
EnsureDefaults(builder);
|
||||
|
||||
if (FileProvider is PhysicalFileProvider physicalFileProvider)
|
||||
{
|
||||
string? outputFile = System.IO.Path.Combine(physicalFileProvider.Root, Path);
|
||||
Factory?.Build(outputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record Write<TConfiguration>(string Section, Action<TConfiguration> UpdateDelegate) where TConfiguration : class;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class WriteHandler<TConfiguration> : IMediatorHandler<Write<TConfiguration>> where TConfiguration : class
|
||||
{
|
||||
private readonly IEventAggregator eventAggregator;
|
||||
private readonly TConfiguration configuration;
|
||||
private readonly IConfigurationWriter<TConfiguration> writer;
|
||||
|
||||
public WriteHandler(TConfiguration configuration,
|
||||
IConfigurationWriter<TConfiguration> writer,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
this.eventAggregator = eventAggregator;
|
||||
this.configuration = configuration;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public void Handle(Write<TConfiguration> request)
|
||||
{
|
||||
request.UpdateDelegate.Invoke(configuration);
|
||||
writer.Write(request.Section, configuration);
|
||||
|
||||
eventAggregator.Publish(new ConfigurationChanged<TConfiguration>(configuration));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonPatch.Net" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0-rc.2.22472.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0-rc.2.22472.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0-rc.2.22472.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0-rc.2.22472.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class EventBuilder : IEventBuilder
|
||||
{
|
||||
private readonly List<IEventBuilderConfiguration> configurations = new();
|
||||
|
||||
public IReadOnlyCollection<IEventBuilderConfiguration> Configurations => new ReadOnlyCollection<IEventBuilderConfiguration>(configurations);
|
||||
|
||||
public IEventBuilderConfiguration<TEvent> Add<TEvent>() where TEvent : class
|
||||
{
|
||||
EventConfiguration<TEvent>? configuration = new();
|
||||
configurations.Add(configuration);
|
||||
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class EventConfiguration<TEvent> : IEventBuilderConfiguration<TEvent> where TEvent : class
|
||||
{
|
||||
private readonly List<IEventDescriptor> descriptors = new();
|
||||
|
||||
public EventConfiguration()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<IEventDescriptor> Descriptors => new ReadOnlyCollection<IEventDescriptor>(descriptors);
|
||||
|
||||
public Action<IServiceProvider, TEvent>? Factory { get; }
|
||||
|
||||
public Action<TEvent>? Next { get; }
|
||||
|
||||
public IEventBuilderConfiguration<TEvent> WithHandler<THandlerEvent>() where THandlerEvent : class
|
||||
{
|
||||
descriptors.Add(new EventDescriptor<TEvent, THandlerEvent>());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IEventBuilderConfiguration<TEvent> WithHandler<THandlerEvent>(Func<TEvent, THandlerEvent> factoryDelegate) where THandlerEvent : class
|
||||
{
|
||||
descriptors.Add(new EventDescriptor<TEvent, THandlerEvent>(factoryDelegate));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IEventBuilderConfiguration<TEvent> WithHandler<THandlerEvent>(Func<IServiceProvider, TEvent, THandlerEvent> factoryDelegate) where THandlerEvent : class
|
||||
{
|
||||
descriptors.Add(new EventDescriptor<TEvent, THandlerEvent>(factoryDelegate));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record EventDescriptor<TEvent, THandlerEvent> : IEventDescriptor
|
||||
{
|
||||
public EventDescriptor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public EventDescriptor(Func<IServiceProvider, TEvent, THandlerEvent>? factoryDelegate)
|
||||
{
|
||||
FactoryDelegate = factoryDelegate;
|
||||
}
|
||||
|
||||
public EventDescriptor(Func<TEvent, THandlerEvent>? nextDelegate)
|
||||
{
|
||||
NextDelegate = nextDelegate;
|
||||
}
|
||||
|
||||
public Func<IServiceProvider, TEvent, THandlerEvent>? FactoryDelegate { get; }
|
||||
|
||||
public Func<TEvent, THandlerEvent>? NextDelegate { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class EventHandler<TTEvent> : IEventHandler<TTEvent> where TTEvent : class
|
||||
{
|
||||
private readonly IEventBuilderConfiguration<TTEvent> configuration;
|
||||
private readonly IDisposer disposer;
|
||||
private readonly IEventAggregator eventAggregator;
|
||||
private readonly IMediator mediator;
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public EventHandler(IEventBuilderConfiguration<TTEvent> configuration,
|
||||
IServiceProvider serviceProvider,
|
||||
IServiceFactory serviceFactory,
|
||||
IEventAggregator eventAggregator,
|
||||
IMediator mediator,
|
||||
IDisposer disposer)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.eventAggregator = eventAggregator;
|
||||
this.mediator = mediator;
|
||||
this.disposer = disposer;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
disposer.Dispose(this);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
disposer.Add(this, eventAggregator.SubscribeUI<TTEvent>(OnEvent));
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnEvent(TTEvent args)
|
||||
{
|
||||
foreach (IEventDescriptor? descriptor in configuration.Descriptors)
|
||||
{
|
||||
mediator.Handle((descriptor as dynamic).NextDelegate?.Invoke(args) ?? (descriptor as dynamic).FactoryDelegate?.Invoke(serviceProvider, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface IEventBuilder
|
||||
{
|
||||
IReadOnlyCollection<IEventBuilderConfiguration> Configurations { get; }
|
||||
|
||||
IEventBuilderConfiguration<TEvent> Add<TEvent>() where TEvent : class;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IEventBuilderConfiguration
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public interface IEventBuilderConfiguration<TEvent> : IEventBuilderConfiguration where TEvent : class
|
||||
{
|
||||
IReadOnlyCollection<IEventDescriptor> Descriptors { get; }
|
||||
|
||||
Action<IServiceProvider, TEvent>? Factory { get; }
|
||||
|
||||
Action<TEvent>? Next { get; }
|
||||
|
||||
IEventBuilderConfiguration<TEvent> WithHandler<THandlerEvent>() where THandlerEvent : class;
|
||||
|
||||
IEventBuilderConfiguration<TEvent> WithHandler<THandlerEvent>(Func<TEvent, THandlerEvent> factoryDelegate) where THandlerEvent : class;
|
||||
|
||||
IEventBuilderConfiguration<TEvent> WithHandler<THandlerEvent>(Func<IServiceProvider, TEvent, THandlerEvent> factoryDelegate) where THandlerEvent : class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IEventDescriptor
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IEventHandler<TTEvent> : IInitializer, IDisposable where TTEvent : class
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class AssemblyExtensions
|
||||
{
|
||||
public static Stream? ExtractResource(this Assembly assembly, string filename)
|
||||
{
|
||||
string? resourceName = $"{assembly.GetName()?.Name?.Replace("-", "_")}.{filename}";
|
||||
return assembly.GetManifestResourceStream(resourceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class ICollectionExtensions
|
||||
{
|
||||
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
|
||||
{
|
||||
foreach (T item in source)
|
||||
{
|
||||
target.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class IDisposableExtensions
|
||||
{
|
||||
public static T Dispose<T>(this T target) where T : IDisposable
|
||||
{
|
||||
target.Dispose();
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TheXamlGuy.Framework.Core.Extensions
|
||||
{
|
||||
public static class IDisposerExtensions
|
||||
{
|
||||
public static void Add(this IDisposer disposer, object subject, IEnumerable<IDisposable> disposers)
|
||||
{
|
||||
disposer.Add(subject, disposers.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public static class IEventAggregatorExtensions
|
||||
{
|
||||
public static IDisposable Subscribe<TEvent>(this IEventAggregator eventAggregator, Action<TEvent> onNext, IScheduler? scheduler = null, Func<TEvent, bool>? where = null)
|
||||
{
|
||||
scheduler ??= Scheduler.Default;
|
||||
where ??= x => true;
|
||||
|
||||
return eventAggregator.GetEvent<TEvent>().Where(where).ObserveOn(scheduler).WeakSubscribe(eventAggregator.Invoker, onNext);
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeUI<TEvent>(this IEventAggregator eventAggregator, Action<TEvent> onNext, Func<TEvent, bool>? where = null)
|
||||
{
|
||||
return eventAggregator.Subscribe(onNext, eventAggregator.Dispatcher, where);
|
||||
}
|
||||
|
||||
public static void Publish<TEvent>(this IEventAggregator eventAggregator) where TEvent : new()
|
||||
{
|
||||
eventAggregator.Publish(new TEvent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class IHostBuilderExtensions
|
||||
{
|
||||
public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, string contentRoot, bool createDirectory)
|
||||
{
|
||||
if (!Directory.Exists(contentRoot) && createDirectory)
|
||||
{
|
||||
Directory.CreateDirectory(contentRoot);
|
||||
}
|
||||
|
||||
return hostBuilder.UseContentRoot(contentRoot);
|
||||
}
|
||||
|
||||
public static IHostBuilder ConfigureEvents(this IHostBuilder hostBuilder, Action<IEventBuilder> builderDelegate)
|
||||
{
|
||||
hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) =>
|
||||
{
|
||||
EventBuilder? builder = new();
|
||||
builderDelegate?.Invoke(builder);
|
||||
|
||||
foreach (IEventBuilderConfiguration? configuration in builder.Configurations)
|
||||
{
|
||||
Type? type = configuration.GetType().GetGenericArguments()[0];
|
||||
|
||||
serviceCollection.AddSingleton(typeof(IEventBuilderConfiguration<>).MakeGenericType(type), configuration);
|
||||
serviceCollection.AddSingleton(typeof(EventHandler<>).MakeGenericType(type));
|
||||
}
|
||||
});
|
||||
|
||||
return hostBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IMediatorAsyncHandler<TRequest>
|
||||
{
|
||||
Task Handle(TRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IMediatorAsyncHandler<TResponse, TRequest>
|
||||
{
|
||||
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class IMediatorExtensions
|
||||
{
|
||||
public static void Handle<TEvent>(this IMediator mediator) where TEvent : new()
|
||||
{
|
||||
mediator.Handle(new TEvent());
|
||||
}
|
||||
|
||||
public static TResponse? Handle<TResponse, TRequest>(this IMediator mediator, params object[] parameters) where TRequest : new()
|
||||
{
|
||||
return mediator.Handle<TResponse?>(new TRequest(), parameters);
|
||||
}
|
||||
|
||||
public static Task HandleAsync<TEvent>(this IMediator mediator) where TEvent : new()
|
||||
{
|
||||
return mediator.HandleAsync(new TEvent());
|
||||
}
|
||||
|
||||
public static Task<TResponse?> HandleAsync<TResponse, TRequest>(this IMediator mediator, params object[] parameters) where TRequest : new()
|
||||
{
|
||||
return mediator.HandleAsync<TResponse?>(new TRequest(), parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IMediatorHandler<TRequest>
|
||||
{
|
||||
void Handle(TRequest request);
|
||||
}
|
||||
|
||||
public interface IMediatorHandler<TResponse, TRequest>
|
||||
{
|
||||
TResponse Handle(TRequest request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class IObservableExtensions
|
||||
{
|
||||
public static IDisposable WeakSubscribe<TEvent>(this IObservable<TEvent> observable, IEventAggregatorInvoker invoker, Action<TEvent> onNext)
|
||||
{
|
||||
MethodInfo methodInfo = onNext.Method;
|
||||
WeakReference weakReference = new(onNext.Target);
|
||||
|
||||
IDisposable? subscription = null;
|
||||
subscription = observable.Subscribe(item =>
|
||||
{
|
||||
if (weakReference.Target is object target)
|
||||
{
|
||||
invoker.Invoke(target, item, methodInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
subscription?.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddConfiguration<TConfiguration>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfiguration : class, new()
|
||||
{
|
||||
serviceCollection.Configure<TConfiguration>(configuration);
|
||||
serviceCollection.AddTransient(provider => provider.GetService<IOptionsMonitor<TConfiguration>>()!.CurrentValue);
|
||||
serviceCollection.AddTransient<ConfigurationInitializer<TConfiguration>>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCreatable<TService, TImplementation>(this IServiceCollection services)
|
||||
{
|
||||
return services.AddSingleton(typeof(IServiceCreator<>).MakeGenericType(typeof(TService)), typeof(ServiceCreator<,>).MakeGenericType(typeof(TService), typeof(TImplementation)));
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCreatable(this IServiceCollection services, Type serviceType, Type implementationType)
|
||||
{
|
||||
return services.AddSingleton(typeof(IServiceCreator<>).MakeGenericType(serviceType), typeof(ServiceCreator<,>).MakeGenericType(serviceType, implementationType));
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCreatableSingleton(this IServiceCollection services, Type serviceType, Type implementationType)
|
||||
{
|
||||
services.AddSingleton(serviceType, implementationType);
|
||||
services.AddCreatable(serviceType, implementationType);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCreatableTransient(this IServiceCollection services, Type serviceType, Type implementationType)
|
||||
{
|
||||
services.AddTransient(serviceType, implementationType);
|
||||
services.AddCreatable(serviceType, implementationType);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddReqiredCore(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddSingleton<IServiceFactory>(provider => new ServiceFactory(provider.GetService, (type, parameters) => ActivatorUtilities.CreateInstance(provider, type, parameters!)))
|
||||
.AddSingleton<IDisposer, Disposer>()
|
||||
.AddSingleton<IEventAggregator, EventAggregator>()
|
||||
.AddSingleton<IEventAggregatorInvoker, EventAggregatorInvoker>()
|
||||
.AddSingleton<IMediator, Mediator>()
|
||||
.AddSingleton<IScope, Scope>()
|
||||
.AddTransient<IPropertyBinderCollection, PropertyBinderCollection>()
|
||||
.AddTransient<IPropertyBuilder, PropertyBuilder>()
|
||||
.AddSingleton<IInitialization, Initialization>(provider => new Initialization(() =>
|
||||
{
|
||||
return services.Where(x => x.ServiceType.GetInterfaces()
|
||||
.Contains(typeof(IInitializer)) || x.ServiceType == typeof(IInitializer))
|
||||
.GroupBy(x => x.ServiceType)
|
||||
.Select(x => x.First())
|
||||
.SelectMany(x => provider.GetServices(x.ServiceType)
|
||||
.Select(x => (IInitializer?)x)).ToList();
|
||||
}))
|
||||
.RegisterHandlers();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddWritableConfiguration<TConfiguration>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfiguration : class, new()
|
||||
{
|
||||
serviceCollection.Configure<TConfiguration>(configuration);
|
||||
serviceCollection.AddTransient<IConfigurationWriter<TConfiguration>, ConfigurationWriter<TConfiguration>>();
|
||||
serviceCollection.AddTransient(provider => provider.GetService<IOptionsMonitor<TConfiguration>>()!.CurrentValue);
|
||||
serviceCollection.AddTransient<IMediatorHandler<Write<TConfiguration>>, WriteHandler<TConfiguration>>();
|
||||
serviceCollection.AddTransient<ConfigurationInitializer<TConfiguration>>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
public static IServiceCollection RegisterHandlers(this IServiceCollection services, params Assembly[] assemblies)
|
||||
{
|
||||
foreach (Tuple<Type, Type> item in GetImplementations(assemblies.Append(Assembly.GetCallingAssembly()), "Handler", typeof(IMediatorHandler<>), typeof(IMediatorHandler<,>), typeof(IMediatorAsyncHandler<>), typeof(IMediatorAsyncHandler<,>)))
|
||||
{
|
||||
if (!item.Item2.GetConstructors().Any(x => x.IsPublic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
services.AddCreatableTransient(item.Item1, item.Item2);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IEnumerable<Tuple<Type, Type>> GetImplementations(IEnumerable<Assembly> assemblies, string endsWith, params Type[] interfaces)
|
||||
{
|
||||
return assemblies.Distinct().SelectMany(a => a.GetTypes())
|
||||
.Where(impl => impl.IsClass && impl.IsPublic && !impl.IsAbstract && impl.Name.EndsWith(endsWith))
|
||||
.SelectMany(impl => impl.GetInterfaces(), (impl, iface) => new { impl, iface })
|
||||
.Where(i => i.iface.IsGenericType && interfaces.Contains(i.iface.GetGenericTypeDefinition()))
|
||||
.Select(x => Tuple.Create(x.iface, x.impl));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class IServiceFactoryExtensions
|
||||
{
|
||||
public static T? Get<T>(this IServiceFactory serviceFactory)
|
||||
{
|
||||
return serviceFactory.Get<T>(typeof(T));
|
||||
}
|
||||
|
||||
public static T Create<T>(this IServiceFactory serviceFactory, params object?[] parameters)
|
||||
{
|
||||
return serviceFactory.Create<T>(typeof(T), parameters);
|
||||
}
|
||||
|
||||
public static object? Create(this IServiceFactory serviceFactory, Type type)
|
||||
{
|
||||
ServiceFactoryDescriptor? descriptor = new(serviceFactory);
|
||||
return descriptor.Create(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static int CountSubstring(this string text, string textToMatch, StringComparison stringComparison)
|
||||
{
|
||||
int count = 0;
|
||||
int minIndex = text.IndexOf(textToMatch, 0, stringComparison);
|
||||
|
||||
while (minIndex != -1)
|
||||
{
|
||||
minIndex = text.IndexOf(textToMatch, minIndex + textToMatch.Length, stringComparison);
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class AppServices : IHostedService
|
||||
{
|
||||
private readonly IMediator mediator;
|
||||
private readonly IDisposer disposer;
|
||||
private readonly IInitialization initialization;
|
||||
|
||||
public AppServices(IMediator mediator,
|
||||
IDisposer disposer,
|
||||
IInitialization initialization)
|
||||
{
|
||||
this.mediator = mediator;
|
||||
this.disposer = disposer;
|
||||
this.initialization = initialization;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
mediator.Handle<Started>();
|
||||
await initialization.InitializeAsync();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
disposer.Dispose(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class Disposer : IDisposer
|
||||
{
|
||||
private readonly ConditionalWeakTable<object, CompositeDisposable> subjects = new();
|
||||
|
||||
public void Add(object subject, params object[] objects)
|
||||
{
|
||||
CompositeDisposable disposables = subjects.GetOrCreateValue(subject);
|
||||
foreach (IDisposable disposable in objects.OfType<IDisposable>())
|
||||
{
|
||||
disposables.Add(disposable);
|
||||
}
|
||||
|
||||
foreach (object notDisposable in objects.Where(x => x is not IDisposable))
|
||||
{
|
||||
disposables.Add(Disposable.Create(() => FromNotDisposable(notDisposable)));
|
||||
}
|
||||
}
|
||||
|
||||
private void FromNotDisposable(object target)
|
||||
{
|
||||
if (target is IEnumerable enumerableTarget)
|
||||
{
|
||||
foreach (object? item in enumerableTarget)
|
||||
{
|
||||
FromNotDisposable(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (target is IDisposable disposableTarget)
|
||||
{
|
||||
disposableTarget.Dispose();
|
||||
}
|
||||
|
||||
if (target is not IDisposable)
|
||||
{
|
||||
Dispose(target);
|
||||
}
|
||||
}
|
||||
|
||||
public TDisposable Replace<TDisposable>(object subject, IDisposable disposer, TDisposable replacement) where TDisposable : IDisposable
|
||||
{
|
||||
CompositeDisposable disposables = subjects.GetOrCreateValue(subject);
|
||||
if (disposer is not null)
|
||||
{
|
||||
disposables.Remove(disposer);
|
||||
}
|
||||
|
||||
disposables.Add(replacement);
|
||||
return replacement;
|
||||
}
|
||||
|
||||
public void Remove(object subject, IDisposable disposer)
|
||||
{
|
||||
CompositeDisposable disposables = subjects.GetOrCreateValue(subject);
|
||||
if (disposer is not null)
|
||||
{
|
||||
disposables.Remove(disposer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose(object subject)
|
||||
{
|
||||
if (subjects.TryGetValue(subject, out CompositeDisposable? disposables))
|
||||
{
|
||||
disposables?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class EventAggregator : IEventAggregator
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, object> subjects;
|
||||
|
||||
public EventAggregator(IEventAggregatorInvoker invoker)
|
||||
{
|
||||
subjects = new ConcurrentDictionary<Type, object>();
|
||||
Dispatcher = new SynchronizationContextScheduler(SynchronizationContext.Current!);
|
||||
Invoker = invoker;
|
||||
|
||||
Current = new EventAggregatorCurrent(this);
|
||||
}
|
||||
|
||||
protected EventAggregator(EventAggregator eventAggregator)
|
||||
{
|
||||
subjects = eventAggregator.subjects;
|
||||
Dispatcher = eventAggregator.Dispatcher;
|
||||
Invoker = eventAggregator.Invoker;
|
||||
Current = eventAggregator.Current;
|
||||
}
|
||||
|
||||
public IEventAggregator Current { get; }
|
||||
|
||||
public IScheduler Dispatcher { get; }
|
||||
|
||||
public IEventAggregatorInvoker Invoker { get; }
|
||||
|
||||
public virtual IObservable<TEvent> GetEvent<TEvent>()
|
||||
{
|
||||
return GetObservable<TEvent>().Skip(1);
|
||||
}
|
||||
|
||||
public virtual ISubject<TEvent> GetSubject<TEvent>()
|
||||
{
|
||||
return (ISubject<TEvent>)subjects.GetOrAdd(typeof(TEvent), x => new BehaviorSubject<TEvent>(default!));
|
||||
}
|
||||
|
||||
public virtual void Publish<TEvent>(TEvent domainEvent)
|
||||
{
|
||||
GetSubject<TEvent>().OnNext(domainEvent);
|
||||
}
|
||||
protected virtual IObservable<TEvent> GetObservable<TEvent>()
|
||||
{
|
||||
return GetSubject<TEvent>().AsObservable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class EventAggregatorCurrent : EventAggregator
|
||||
{
|
||||
public EventAggregatorCurrent(EventAggregator eventAggregator) : base(eventAggregator)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override IObservable<TEvent> GetEvent<TEvent>()
|
||||
{
|
||||
return GetObservable<TEvent>().Skip(0).Where(x => x != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class EventAggregatorInvoker : IEventAggregatorInvoker
|
||||
{
|
||||
private readonly IScope scope;
|
||||
|
||||
public EventAggregatorInvoker(IScope scope)
|
||||
{
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public void Invoke<TEvent>(object target, TEvent item, MethodInfo methodInfo)
|
||||
{
|
||||
using (scope.Enter<TEvent>(target))
|
||||
{
|
||||
methodInfo.Invoke(target, new object[] { item! });
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInvoking<TEvent>(object target)
|
||||
{
|
||||
return scope.IsActive<TEvent>(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface ICachable
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IDisposer
|
||||
{
|
||||
void Add(object subject, params object[] objects);
|
||||
|
||||
TDisposable Replace<TDisposable>(object subject, IDisposable disposer, TDisposable replacement) where TDisposable : IDisposable;
|
||||
|
||||
void Remove(object subject, IDisposable disposer);
|
||||
|
||||
void Dispose(object subject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IEventAggregator
|
||||
{
|
||||
IEventAggregator Current { get; }
|
||||
|
||||
IScheduler Dispatcher { get; }
|
||||
|
||||
IEventAggregatorInvoker Invoker { get; }
|
||||
|
||||
IObservable<TEvent> GetEvent<TEvent>();
|
||||
|
||||
ISubject<TEvent> GetSubject<TEvent>();
|
||||
|
||||
void Publish<TEvent>(TEvent args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IEventAggregatorInvoker
|
||||
{
|
||||
void Invoke<TEvent>(object target, TEvent item, MethodInfo methodInfo);
|
||||
|
||||
bool IsInvoking<TEvent>(object target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface IHasSensorPlacement
|
||||
{
|
||||
SensorPlacement Placement { get; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IInitialization
|
||||
{
|
||||
Task InitializeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IInitializer
|
||||
{
|
||||
Task InitializeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IKeepAlive
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IMediator
|
||||
{
|
||||
void Handle(object request, params object[] parameters);
|
||||
|
||||
TResponse? Handle<TResponse>(object request, params object[] parameters);
|
||||
|
||||
Task HandleAsync(object request, params object[] parameters);
|
||||
|
||||
Task HandleAsync(object request, CancellationToken cancellationToken, params object[] parameters);
|
||||
|
||||
Task<TResponse?> HandleAsync<TResponse>(object request, params object[] parameters);
|
||||
|
||||
Task<TResponse?> HandleAsync<TResponse>(object request, CancellationToken cancellationToken, params object[] parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IScope
|
||||
{
|
||||
IDisposable Enter<T>(object target);
|
||||
|
||||
bool IsActive<T>(object target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IServiceCreator<I>
|
||||
{
|
||||
object Create(Func<Type, object[], object> creator, params object[] parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IServiceFactory
|
||||
{
|
||||
T? Get<T>(Type type);
|
||||
|
||||
T Create<T>(Type type, params object?[] parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface ITwoStateSensor
|
||||
{
|
||||
SensorState State { get; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class Initialization : IInitialization
|
||||
{
|
||||
private readonly Func<IEnumerable<IInitializer?>> factory;
|
||||
|
||||
public Initialization(Func<IEnumerable<IInitializer?>> factory)
|
||||
{
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
foreach (IInitializer? initializer in factory())
|
||||
{
|
||||
if (initializer is not null)
|
||||
{
|
||||
await initializer.InitializeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class Mediator : IMediator
|
||||
{
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
|
||||
public Mediator(IServiceFactory serviceFactory)
|
||||
{
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
public void Handle(object request, params object[] parameters)
|
||||
{
|
||||
if (GetHandler(typeof(IMediatorHandler<>).MakeGenericType(request.GetType()), parameters) is { } handler)
|
||||
{
|
||||
handler.Handle((dynamic)request);
|
||||
}
|
||||
}
|
||||
|
||||
public TResponse? Handle<TResponse>(object request, params object[] parameters)
|
||||
{
|
||||
if (GetHandler(typeof(IMediatorHandler<,>).MakeGenericType(typeof(TResponse), request.GetType()), parameters) is { } handler)
|
||||
{
|
||||
return handler.Handle((dynamic)request);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public Task HandleAsync(object request, CancellationToken cancellationToken, params object[] parameters)
|
||||
{
|
||||
if (GetHandler(typeof(IMediatorAsyncHandler<>).MakeGenericType(request.GetType()), parameters) is { } handler)
|
||||
{
|
||||
return handler.Handle((dynamic)request, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(object request, params object[] parameters)
|
||||
{
|
||||
return HandleAsync(request, CancellationToken.None, parameters);
|
||||
}
|
||||
|
||||
public Task<TResponse?> HandleAsync<TResponse>(object request, CancellationToken cancellationToken, params object[] parameters)
|
||||
{
|
||||
if (GetHandler(typeof(IMediatorAsyncHandler<,>).MakeGenericType(typeof(TResponse), request.GetType()), parameters) is { } handler)
|
||||
{
|
||||
return handler.Handle((dynamic)request, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.FromResult<TResponse?>(default);
|
||||
}
|
||||
|
||||
public Task<TResponse?> HandleAsync<TResponse>(object request, params object[] parameters)
|
||||
{
|
||||
return HandleAsync<TResponse?>(request, CancellationToken.None, parameters);
|
||||
}
|
||||
|
||||
private dynamic? GetHandler(Type type, params object[] parameters)
|
||||
{
|
||||
return parameters.Length == 0 ? serviceFactory.Get<object>(type) : serviceFactory.Create<object>(type, parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class Scope : IScope
|
||||
{
|
||||
private readonly ConcurrentDictionary<object, bool> scopes = new ConcurrentDictionary<object, bool>();
|
||||
|
||||
public IDisposable Enter<T>(object target)
|
||||
{
|
||||
scopes.TryAdd(Tuple.Create(target, typeof(T)), true);
|
||||
return Disposable.Create(() => scopes.TryRemove(Tuple.Create(target, typeof(T)), out bool value));
|
||||
}
|
||||
|
||||
public bool IsActive<T>(object target)
|
||||
{
|
||||
return scopes.ContainsKey(Tuple.Create(target, typeof(T)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public enum SensorPlacement
|
||||
{
|
||||
Left = 0,
|
||||
Top = 1,
|
||||
Right = 3,
|
||||
Bottom = 4,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public enum SensorState
|
||||
{
|
||||
Off = 0,
|
||||
On = 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class ServiceCreator<I, T> : IServiceCreator<I>
|
||||
{
|
||||
public virtual object Create(Func<Type, object[], object> creator, params object[] parameters)
|
||||
{
|
||||
return creator(typeof(T), parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class ServiceFactory : IServiceFactory
|
||||
{
|
||||
private readonly Func<Type, object?> factory;
|
||||
private readonly Func<Type, object?[], object> creator;
|
||||
|
||||
public ServiceFactory(Func<Type, object?> factory, Func<Type, object?[], object> creator)
|
||||
{
|
||||
this.factory = factory;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
public T? Get<T>(Type type)
|
||||
{
|
||||
T? value = (T?)factory(type);
|
||||
return value;
|
||||
}
|
||||
|
||||
public T Create<T>(Type type, params object?[] parameters)
|
||||
{
|
||||
dynamic? lookup = factory(typeof(IServiceCreator<>).MakeGenericType(type));
|
||||
return lookup is not null ? (T)lookup.Create(creator, parameters) : (T)creator(type, parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
internal class ServiceFactoryDescriptor
|
||||
{
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
|
||||
public ServiceFactoryDescriptor(IServiceFactory serviceFactory)
|
||||
{
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
public object? Create(Type type)
|
||||
{
|
||||
MethodInfo? methodInfo = typeof(ServiceFactoryDescriptor).GetMethod(nameof(Create), BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return methodInfo?.MakeGenericMethod(type).Invoke(this, new object[] { type });
|
||||
}
|
||||
|
||||
private T Create<T>(Type type)
|
||||
{
|
||||
return serviceFactory.Create<T>(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record Started;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IObservableViewModel : INotifyDataErrorInfo, IDisposable
|
||||
{
|
||||
void Initialize();
|
||||
|
||||
bool IsInitialized { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public virtual void OnPropertyChanged(string propertyName, object? before, object? after)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class ObservableViewModel : ObservableObject, IObservableViewModel
|
||||
{
|
||||
public ObservableViewModel(IPropertyBuilder propertyBuilder,
|
||||
IEventAggregator eventAggregator,
|
||||
IServiceFactory serviceFactory,
|
||||
IDisposer disposer)
|
||||
{
|
||||
PropertyBuilder = propertyBuilder;
|
||||
EventAggregator = eventAggregator;
|
||||
ServiceFactory = serviceFactory;
|
||||
Disposer = disposer;
|
||||
|
||||
ValidationErrors = new PropertyValidationError<string, string>();
|
||||
}
|
||||
|
||||
public event System.EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||
|
||||
public IDisposer Disposer { get; }
|
||||
|
||||
public IEventAggregator EventAggregator { get; }
|
||||
|
||||
public bool HasErrors => ValidationErrors.Count > 0;
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
public IPropertyBuilder PropertyBuilder { get; }
|
||||
|
||||
public IServiceFactory ServiceFactory { get; }
|
||||
|
||||
public PropertyValidationError<string, string> ValidationErrors { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
OnDisposing();
|
||||
|
||||
Disposer.Dispose(this);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public IEnumerable GetErrors(string? propertyName)
|
||||
{
|
||||
return propertyName is not null && ValidationErrors.Contains(propertyName) ? ValidationErrors[propertyName]! : Array.Empty<string>();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsInitialized = true;
|
||||
OnInitialize();
|
||||
}
|
||||
|
||||
public override void OnPropertyChanged(string propertyName, object? before, object? after)
|
||||
{
|
||||
SetProperty(propertyName, false);
|
||||
base.OnPropertyChanged(propertyName, before, after);
|
||||
}
|
||||
|
||||
protected virtual void ClearValidationErrors()
|
||||
{
|
||||
foreach (PropertyBinder? binder in PropertyBuilder.Binders)
|
||||
{
|
||||
if (binder.PropertyName is { })
|
||||
{
|
||||
if (ValidationErrors.Contains(binder.PropertyName))
|
||||
{
|
||||
ValidationErrors.Remove(binder.PropertyName);
|
||||
OnErrorsChanged(binder.PropertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ValidationErrors), null, null);
|
||||
}
|
||||
|
||||
protected virtual void OnDisposing()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void OnErrorsChanged(string propertyName)
|
||||
{
|
||||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected virtual void OnInitialize()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected void SetProperty(string propertyName)
|
||||
{
|
||||
SetProperty(propertyName, true);
|
||||
}
|
||||
|
||||
protected void SetProperty(string? propertyName, bool isExplicit)
|
||||
{
|
||||
if (propertyName is { })
|
||||
{
|
||||
if (PropertyBuilder.Binders.TryGet(propertyName, out PropertyBinder? binder) && binder is { })
|
||||
{
|
||||
if (binder.Mode == PropertyChangedMode.Explicit && !isExplicit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearValidationError(propertyName);
|
||||
if (!binder.TryValidate(out string? message) && message is { })
|
||||
{
|
||||
AddValidationError(propertyName, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool Validate(bool clearPreviousErrors = true)
|
||||
{
|
||||
if (clearPreviousErrors)
|
||||
{
|
||||
ClearValidationErrors();
|
||||
}
|
||||
|
||||
foreach (PropertyBinder? binder in PropertyBuilder.Binders)
|
||||
{
|
||||
if (binder.PropertyName is { })
|
||||
{
|
||||
if (!binder.TryValidate(out string? message) && message is { })
|
||||
{
|
||||
AddValidationError(binder.PropertyName, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !HasErrors;
|
||||
}
|
||||
|
||||
private void AddValidationError(string propertyName, string validationMessage)
|
||||
{
|
||||
if (propertyName is { })
|
||||
{
|
||||
OnErrorsChanged(propertyName);
|
||||
ValidationErrors[propertyName] = validationMessage;
|
||||
|
||||
OnPropertyChanged(nameof(ValidationErrors), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearValidationError(string propertyName)
|
||||
{
|
||||
if (ValidationErrors.Contains(propertyName))
|
||||
{
|
||||
ValidationErrors.Remove(propertyName);
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ValidationErrors), null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class ObservableViewModelCollection<TItemViewModel> :
|
||||
ObservableCollection<TItemViewModel>, IObservableViewModel
|
||||
where TItemViewModel : class
|
||||
{
|
||||
private readonly ObservableCollection<TItemViewModel>? source;
|
||||
|
||||
public ObservableViewModelCollection(IPropertyBuilder propertyBuilder,
|
||||
IEventAggregator eventAggregator,
|
||||
IServiceFactory serviceFactory,
|
||||
IDisposer disposer,
|
||||
ObservableCollection<TItemViewModel> source)
|
||||
{
|
||||
PropertyBuilder = propertyBuilder;
|
||||
EventAggregator = eventAggregator;
|
||||
ServiceFactory = serviceFactory;
|
||||
Disposer = disposer;
|
||||
|
||||
this.source = source;
|
||||
source.CollectionChanged += OnSourceCollectionChanged;
|
||||
|
||||
AddRange(source);
|
||||
|
||||
ValidationErrors = new PropertyValidationError<string, string>();
|
||||
}
|
||||
|
||||
public ObservableViewModelCollection(IPropertyBuilder propertyBuilder,
|
||||
IEventAggregator eventAggregator,
|
||||
IServiceFactory serviceFactory,
|
||||
IDisposer disposer)
|
||||
{
|
||||
PropertyBuilder = propertyBuilder;
|
||||
EventAggregator = eventAggregator;
|
||||
ServiceFactory = serviceFactory;
|
||||
Disposer = disposer;
|
||||
|
||||
ValidationErrors = new PropertyValidationError<string, string>();
|
||||
}
|
||||
|
||||
public event System.EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||
|
||||
public IDisposer Disposer { get; }
|
||||
|
||||
public IEventAggregator EventAggregator { get; }
|
||||
|
||||
public bool HasErrors => ValidationErrors.Count > 0;
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
public IPropertyBuilder PropertyBuilder { get; }
|
||||
|
||||
public IServiceFactory ServiceFactory { get; }
|
||||
|
||||
public PropertyValidationError<string, string> ValidationErrors { get; }
|
||||
|
||||
public TItemViewModel Add()
|
||||
{
|
||||
TItemViewModel? item = ServiceFactory.Create<TItemViewModel>();
|
||||
Disposer.Add(this, item);
|
||||
|
||||
base.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public TItemViewModel Add<T>() where T : TItemViewModel
|
||||
{
|
||||
T? item = ServiceFactory.Create<T>();
|
||||
Disposer.Add(this, item);
|
||||
base.Add(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public TItemViewModel Add<T>(params object?[] parameters) where T : TItemViewModel
|
||||
{
|
||||
T? item = ServiceFactory.Create<T>(parameters);
|
||||
|
||||
Disposer.Add(this, item);
|
||||
Disposer.Add(item, Disposable.Create(() =>
|
||||
{
|
||||
if (!isClearing)
|
||||
{
|
||||
if (Contains(item))
|
||||
{
|
||||
Remove(item);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
base.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public new TItemViewModel Add(TItemViewModel item)
|
||||
{
|
||||
Disposer.Add(this, item);
|
||||
Disposer.Add(item, Disposable.Create(() =>
|
||||
{
|
||||
if (!isClearing)
|
||||
{
|
||||
if (Contains(item))
|
||||
{
|
||||
Remove(item);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
base.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public TItemViewModel Add(params object?[] parameters)
|
||||
{
|
||||
TItemViewModel? item = ServiceFactory.Create<TItemViewModel>(parameters);
|
||||
|
||||
Disposer.Add(this, item);
|
||||
Disposer.Add(item, Disposable.Create(() =>
|
||||
{
|
||||
if (!isClearing)
|
||||
{
|
||||
if (Contains(item))
|
||||
{
|
||||
Remove(item);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
base.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public void AddRange(ICollection<TItemViewModel> items)
|
||||
{
|
||||
foreach (TItemViewModel? item in items)
|
||||
{
|
||||
AddItemToDisposer(item);
|
||||
base.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private bool isClearing;
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
isClearing = true;
|
||||
|
||||
foreach (TItemViewModel? item in this)
|
||||
{
|
||||
if (item is not IKeepAlive)
|
||||
{
|
||||
Disposer.Dispose(item);
|
||||
}
|
||||
}
|
||||
|
||||
base.Clear();
|
||||
isClearing = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
OnDisposing();
|
||||
|
||||
if (source is not null)
|
||||
{
|
||||
source.CollectionChanged -= OnSourceCollectionChanged;
|
||||
}
|
||||
|
||||
Disposer.Dispose(this);
|
||||
Clear();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public IEnumerable GetErrors(string? propertyName)
|
||||
{
|
||||
return propertyName is not null && ValidationErrors.Contains(propertyName) ? ValidationErrors[propertyName]! : Array.Empty<string>();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsInitialized = true;
|
||||
OnInitialize();
|
||||
}
|
||||
|
||||
public void Insert<TItem>(params object[] parameters) where TItem : TItemViewModel
|
||||
{
|
||||
TItem? item = ServiceFactory.Create<TItem>(parameters);
|
||||
AddItemToDisposer(item);
|
||||
|
||||
base.Add(item);
|
||||
}
|
||||
|
||||
public new void Insert(int index, TItemViewModel item)
|
||||
{
|
||||
AddItemToDisposer(item);
|
||||
base.Insert(index, item);
|
||||
}
|
||||
|
||||
public void Insert<TItem>(int index, params object[] parameters) where TItem : TItemViewModel
|
||||
{
|
||||
TItem? item = ServiceFactory.Create<TItem>(parameters);
|
||||
AddItemToDisposer(item);
|
||||
|
||||
base.Insert(index, item);
|
||||
}
|
||||
|
||||
public void Insert(TItemViewModel item)
|
||||
{
|
||||
base.Insert(0, item);
|
||||
AddItemToDisposer(item);
|
||||
}
|
||||
|
||||
public new void Remove(TItemViewModel item)
|
||||
{
|
||||
if (item is not IKeepAlive)
|
||||
{
|
||||
Disposer.Dispose(item);
|
||||
}
|
||||
|
||||
base.Remove(item);
|
||||
}
|
||||
|
||||
protected virtual void ClearValidationErrors()
|
||||
{
|
||||
foreach (PropertyBinder? binder in PropertyBuilder.Binders)
|
||||
{
|
||||
if (binder.PropertyName is { })
|
||||
{
|
||||
if (ValidationErrors.Contains(binder.PropertyName))
|
||||
{
|
||||
ValidationErrors.Remove(binder.PropertyName);
|
||||
OnErrorsChanged(binder.PropertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ValidationErrors)));
|
||||
}
|
||||
|
||||
protected virtual void OnDisposing()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void OnErrorsChanged(string propertyName)
|
||||
{
|
||||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected virtual void OnInitialize()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(PropertyChangedEventArgs args)
|
||||
{
|
||||
SetProperty(args.PropertyName, false);
|
||||
base.OnPropertyChanged(args);
|
||||
}
|
||||
|
||||
protected void SetProperty(string? propertyName, bool isExplicit)
|
||||
{
|
||||
if (propertyName is { })
|
||||
{
|
||||
if (PropertyBuilder.Binders.TryGet(propertyName, out PropertyBinder? binder) && binder is { })
|
||||
{
|
||||
if (binder.Mode == PropertyChangedMode.Explicit && !isExplicit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearValidationError(propertyName);
|
||||
if (!binder.TryValidate(out string? message) && message is { })
|
||||
{
|
||||
AddValidationError(propertyName, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool Validate(bool clearPreviousErrors = true)
|
||||
{
|
||||
if (clearPreviousErrors)
|
||||
{
|
||||
ClearValidationErrors();
|
||||
}
|
||||
|
||||
foreach (PropertyBinder? binder in PropertyBuilder.Binders)
|
||||
{
|
||||
if (binder.PropertyName is { })
|
||||
{
|
||||
if (!binder.TryValidate(out string? message) && message is { })
|
||||
{
|
||||
AddValidationError(binder.PropertyName, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !HasErrors;
|
||||
}
|
||||
|
||||
private void AddItemToDisposer(TItemViewModel? item)
|
||||
{
|
||||
if (item is not IKeepAlive)
|
||||
{
|
||||
Disposer.Add(this, item!);
|
||||
}
|
||||
}
|
||||
private void AddValidationError(string propertyName, string validationMessage)
|
||||
{
|
||||
if (propertyName is { })
|
||||
{
|
||||
OnErrorsChanged(propertyName);
|
||||
ValidationErrors[propertyName] = validationMessage;
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ValidationErrors)));
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearValidationError(string propertyName)
|
||||
{
|
||||
if (ValidationErrors.Contains(propertyName))
|
||||
{
|
||||
ValidationErrors.Remove(propertyName);
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ValidationErrors)));
|
||||
}
|
||||
|
||||
private void OnSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
SynchronizeCollection(this, args);
|
||||
}
|
||||
|
||||
private void SynchronizeCollection(ObservableCollection<TItemViewModel> target, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
TItemViewModel[]? newItems = args.NewItems?.Cast<TItemViewModel>().ToArray();
|
||||
TItemViewModel[]? oldItems = args.OldItems?.Cast<TItemViewModel>().ToArray();
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (newItems is not null)
|
||||
{
|
||||
for (int index = 0; index < newItems.Length; index++)
|
||||
{
|
||||
target.Insert(args.NewStartingIndex + index, newItems[index]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
if (oldItems is not null)
|
||||
{
|
||||
for (int index = 0; index < oldItems.Length; index++)
|
||||
{
|
||||
RemoveAt(args.OldStartingIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
if (oldItems is not null)
|
||||
{
|
||||
for (int index = 0; index < oldItems.Length; index++)
|
||||
{
|
||||
target.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
if (newItems is not null)
|
||||
{
|
||||
for (int index = 0; index < newItems.Length; index++)
|
||||
{
|
||||
target.Insert(index, newItems[index]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
target.Clear();
|
||||
if (newItems is not null)
|
||||
{
|
||||
for (int index = 0; index < newItems.Length; index++)
|
||||
{
|
||||
target.Insert(index, newItems[index]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IRouterContext : IInitializer
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record Navigate
|
||||
{
|
||||
public Navigate(string name, params object[] parameters)
|
||||
{
|
||||
Name = name;
|
||||
Parameters = parameters;
|
||||
}
|
||||
|
||||
public Navigate(Type type, params object[] parameters)
|
||||
{
|
||||
Type = type;
|
||||
Parameters = parameters;
|
||||
}
|
||||
|
||||
public Type? Type { get; }
|
||||
|
||||
public object? Route { get; init; }
|
||||
|
||||
public string? Name { get; }
|
||||
|
||||
public string? FriendlyName { get; init; }
|
||||
|
||||
public object[] Parameters { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record NavigateBack(object Route);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public record Show(Type ViewModelType, params object[] Parameters);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface INamedDataTemplateFactory
|
||||
{
|
||||
object? Create(string name, params object[] parameters);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface INamedTemplateFactory
|
||||
{
|
||||
object? Create(string name);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface ITemplateBuilder
|
||||
{
|
||||
IReadOnlyCollection<ITemplateDescriptor> Descriptors { get; }
|
||||
|
||||
ITemplateBuilder Add<TViewModel, TView>(string name, ServiceLifetime lifetime = ServiceLifetime.Transient);
|
||||
|
||||
ITemplateBuilder Add<TViewModel, TView>(ServiceLifetime lifetime = ServiceLifetime.Transient);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface ITemplateDescriptor
|
||||
{
|
||||
Type DataType { get; }
|
||||
|
||||
ServiceLifetime Lifetime { get; }
|
||||
|
||||
string? Name { get; }
|
||||
|
||||
Type TemplateType { get; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface ITemplateDescriptorProvider
|
||||
{
|
||||
ITemplateDescriptor? Get(string name);
|
||||
|
||||
ITemplateDescriptor? Get(Type type);
|
||||
|
||||
ITemplateDescriptor? Get<T>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface ITemplateFactory
|
||||
{
|
||||
object? Create([MaybeNull] object? data);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface ITemplateSelector
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public interface ITypedDataTemplateFactory
|
||||
{
|
||||
object? Create(Type type, params object[] parameters);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class NamedDataTemplateFactory : INamedDataTemplateFactory
|
||||
{
|
||||
private readonly Dictionary<string, object> dataTracking = new();
|
||||
|
||||
private readonly IReadOnlyCollection<ITemplateDescriptor> descriptors;
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
|
||||
public NamedDataTemplateFactory(IReadOnlyCollection<ITemplateDescriptor> descriptors,
|
||||
IServiceFactory serviceFactory)
|
||||
{
|
||||
this.descriptors = descriptors;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
public virtual object? Create(string name, params object[] parameters)
|
||||
{
|
||||
if (dataTracking.TryGetValue(name, out object? data))
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
if (descriptors.FirstOrDefault(x => x.Name == name) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
data = parameters is { Length: > 0 } ? serviceFactory.Create<object>(descriptor.DataType, parameters) : serviceFactory.Get<object>(descriptor.DataType);
|
||||
if (data is ICachable cachable)
|
||||
{
|
||||
dataTracking[name] = cachable;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class NamedTemplateFactory : INamedTemplateFactory
|
||||
{
|
||||
private readonly Dictionary<string, object> dataTracking = new();
|
||||
|
||||
private readonly ITemplateDescriptorProvider provider;
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
|
||||
public NamedTemplateFactory(ITemplateDescriptorProvider provider,
|
||||
IServiceFactory serviceFactory)
|
||||
{
|
||||
this.provider = provider;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
public virtual object? Create(string name)
|
||||
{
|
||||
if (dataTracking.TryGetValue(name, out object? view))
|
||||
{
|
||||
return view;
|
||||
}
|
||||
|
||||
if (provider.Get(name) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
view = serviceFactory.Get<object>(descriptor.TemplateType);
|
||||
if (view is ICachable cachable)
|
||||
{
|
||||
dataTracking[name] = cachable;
|
||||
}
|
||||
|
||||
if (descriptor.GetType().GenericTypeArguments is { Length: 2 })
|
||||
{
|
||||
(descriptor as dynamic).ViewInvoker?.Invoke(view);
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class TemplateBuilder : ITemplateBuilder
|
||||
{
|
||||
private readonly List<ITemplateDescriptor> descriptors = new();
|
||||
|
||||
public IReadOnlyCollection<ITemplateDescriptor> Descriptors => new ReadOnlyCollection<ITemplateDescriptor>(descriptors);
|
||||
|
||||
public ITemplateBuilder Add<TViewModel, TView>(string name, ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
descriptors.Add(new TemplateDescriptor(typeof(TViewModel), typeof(TView), name, lifetime));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ITemplateBuilder Add<TViewModel, TView>(ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
descriptors.Add(new TemplateDescriptor(typeof(TViewModel), typeof(TView), null, lifetime));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class TemplateDescriptor : ITemplateDescriptor
|
||||
{
|
||||
public TemplateDescriptor(Type dataType,
|
||||
Type templateType,
|
||||
string? name = null,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
TemplateType = templateType;
|
||||
DataType = dataType;
|
||||
Name = name;
|
||||
Lifetime = lifetime;
|
||||
}
|
||||
|
||||
public ServiceLifetime Lifetime { get; }
|
||||
|
||||
public Type TemplateType { get; }
|
||||
|
||||
public Type DataType { get; }
|
||||
|
||||
public string? Name { get; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class TemplateDescriptorProvider : ITemplateDescriptorProvider
|
||||
{
|
||||
private readonly IReadOnlyCollection<ITemplateDescriptor> descriptors;
|
||||
|
||||
public TemplateDescriptorProvider(IReadOnlyCollection<ITemplateDescriptor> descriptors)
|
||||
{
|
||||
this.descriptors = descriptors;
|
||||
}
|
||||
|
||||
public ITemplateDescriptor? Get(string name)
|
||||
{
|
||||
if (descriptors.FirstOrDefault(x => x.Name == name) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ITemplateDescriptor? Get(Type type)
|
||||
{
|
||||
if (descriptors.FirstOrDefault(x => x.DataType == type) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ITemplateDescriptor? Get<T>()
|
||||
{
|
||||
if (descriptors.FirstOrDefault(x => x.DataType == typeof(T)) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class TemplateFactory : ITemplateFactory
|
||||
{
|
||||
private readonly Dictionary<object, object> dataTracking = new();
|
||||
|
||||
private readonly ITemplateDescriptorProvider provider;
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
|
||||
public TemplateFactory(ITemplateDescriptorProvider provider,
|
||||
IServiceFactory serviceFactory)
|
||||
{
|
||||
this.provider = provider;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
public virtual object? Create([MaybeNull] object? data)
|
||||
{
|
||||
if (data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dataTracking.TryGetValue(data, out object? template))
|
||||
{
|
||||
return template;
|
||||
}
|
||||
|
||||
if (provider.Get(data.GetType()) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
template = serviceFactory.Get<object>(descriptor.TemplateType);
|
||||
if (template is ICachable cachable)
|
||||
{
|
||||
dataTracking[data] = cachable;
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace TheXamlGuy.Framework.Core;
|
||||
|
||||
public class TypedDataTemplateFactory : ITypedDataTemplateFactory
|
||||
{
|
||||
private readonly Dictionary<Type, object> dataTracking = new();
|
||||
|
||||
private readonly IReadOnlyCollection<ITemplateDescriptor> descriptors;
|
||||
private readonly IServiceFactory serviceFactory;
|
||||
|
||||
public TypedDataTemplateFactory(IReadOnlyCollection<ITemplateDescriptor> descriptors,
|
||||
IServiceFactory serviceFactory)
|
||||
{
|
||||
this.descriptors = descriptors;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
public virtual object? Create(Type type, params object[] parameters)
|
||||
{
|
||||
if (dataTracking.TryGetValue(type, out object? data))
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
if (descriptors.FirstOrDefault(x => x.DataType == type) is ITemplateDescriptor descriptor)
|
||||
{
|
||||
data = parameters is { Length: > 0 } ? serviceFactory.Create<object>(descriptor.DataType, parameters) : serviceFactory.Get<object>(descriptor.DataType);
|
||||
if (data is ICachable cachable)
|
||||
{
|
||||
dataTracking[type] = cachable;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IPropertyBinderCollection : IReadOnlyCollection<PropertyBinder>
|
||||
{
|
||||
void Add(string key, PropertyBinder binder);
|
||||
|
||||
bool TryGet(string key, [MaybeNull] out PropertyBinder? value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public interface IPropertyBuilder
|
||||
{
|
||||
IPropertyBinderCollection Binders { get; }
|
||||
|
||||
void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged);
|
||||
|
||||
void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged,
|
||||
PropertyValidation propertyChangedValidation);
|
||||
|
||||
void Add<TProperty>(Expression<Func<TProperty>> property, PropertyValidation propertyChangedValidation);
|
||||
|
||||
void Add<TProperty>(Expression<Func<TProperty>> property, PropertyValidation propertyChangedValidation,
|
||||
PropertyChangedMode mode);
|
||||
|
||||
void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged,
|
||||
PropertyValidation propertyChangedValidation, PropertyChangedMode mode);
|
||||
|
||||
void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged, PropertyChangedMode mode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class PropertyBinder
|
||||
{
|
||||
private readonly Action? propertyChanged;
|
||||
private readonly PropertyValidation? propertyValidation;
|
||||
|
||||
internal PropertyBinder(string propertyName, Action propertyChanged)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
this.propertyChanged = propertyChanged;
|
||||
}
|
||||
|
||||
internal PropertyBinder(string propertyName,
|
||||
Action propertyChanged,
|
||||
PropertyChangedMode mode)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
Mode = mode;
|
||||
|
||||
this.propertyChanged = propertyChanged;
|
||||
}
|
||||
|
||||
internal PropertyBinder(string propertyName,
|
||||
Action propertyChanged,
|
||||
PropertyValidation validation,
|
||||
PropertyChangedMode mode)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
Mode = mode;
|
||||
|
||||
this.propertyChanged = propertyChanged;
|
||||
propertyValidation = validation;
|
||||
}
|
||||
|
||||
internal PropertyBinder(string propertyName,
|
||||
Action propertyChanged,
|
||||
PropertyValidation validation)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
|
||||
this.propertyChanged = propertyChanged;
|
||||
propertyValidation = validation;
|
||||
}
|
||||
|
||||
internal PropertyBinder(string propertyName, PropertyValidation validation)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
propertyValidation = validation;
|
||||
}
|
||||
|
||||
internal PropertyBinder(string propertyName, PropertyValidation validation, PropertyChangedMode mode)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
Mode = mode;
|
||||
|
||||
propertyValidation = validation;
|
||||
}
|
||||
|
||||
public PropertyChangedMode Mode { get; }
|
||||
|
||||
public string? PropertyName { get; }
|
||||
|
||||
public void Set()
|
||||
{
|
||||
propertyChanged?.Invoke();
|
||||
}
|
||||
|
||||
public bool TryValidate([MaybeNull] out string message)
|
||||
{
|
||||
message = "";
|
||||
|
||||
if (propertyValidation is not null && propertyValidation.Validation?.Invoke() == false)
|
||||
{
|
||||
message = propertyValidation.Message;
|
||||
return false;
|
||||
}
|
||||
|
||||
propertyChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class PropertyBinderCollection : IPropertyBinderCollection
|
||||
{
|
||||
private readonly Dictionary<string, PropertyBinder> binders = new();
|
||||
|
||||
public int Count => binders.Count;
|
||||
|
||||
public void Add(string key, PropertyBinder binder)
|
||||
{
|
||||
binders.Add(key, binder);
|
||||
}
|
||||
|
||||
public IEnumerator<PropertyBinder> GetEnumerator()
|
||||
{
|
||||
return binders.Select(x => x.Value).GetEnumerator();
|
||||
}
|
||||
|
||||
public bool TryGet(string key, [MaybeNull] out PropertyBinder? value)
|
||||
{
|
||||
return binders.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return binders.Select(x => x.Value).GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class PropertyBuilder : IPropertyBuilder
|
||||
{
|
||||
public PropertyBuilder(IPropertyBinderCollection binders)
|
||||
{
|
||||
Binders = binders;
|
||||
}
|
||||
|
||||
public IPropertyBinderCollection Binders { get; }
|
||||
|
||||
public void Add<TProperty>(Expression<Func<TProperty>> property, PropertyValidation propertyChangedValidation)
|
||||
{
|
||||
string? name = GetPropertyName(property);
|
||||
Binders.Add(name, new PropertyBinder(name, propertyChangedValidation));
|
||||
}
|
||||
|
||||
public void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged,
|
||||
PropertyValidation propertyChangedValidation)
|
||||
{
|
||||
string? name = GetPropertyName(property);
|
||||
Binders.Add(name, new PropertyBinder(name, propertyChanged, propertyChangedValidation));
|
||||
}
|
||||
|
||||
public void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged)
|
||||
{
|
||||
string? name = GetPropertyName(property);
|
||||
Binders.Add(name, new PropertyBinder(name, propertyChanged));
|
||||
}
|
||||
|
||||
public void Add<TProperty>(Expression<Func<TProperty>> property, PropertyValidation propertyChangedValidation, PropertyChangedMode mode)
|
||||
{
|
||||
string? name = GetPropertyName(property);
|
||||
Binders.Add(name, new PropertyBinder(name, propertyChangedValidation, mode));
|
||||
}
|
||||
|
||||
public void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged, PropertyValidation propertyChangedValidation, PropertyChangedMode mode)
|
||||
{
|
||||
string? name = GetPropertyName(property);
|
||||
Binders.Add(name, new PropertyBinder(name, propertyChanged, propertyChangedValidation, mode));
|
||||
}
|
||||
|
||||
public void Add<TProperty>(Expression<Func<TProperty>> property, Action propertyChanged, PropertyChangedMode mode)
|
||||
{
|
||||
string? name = GetPropertyName(property);
|
||||
Binders.Add(name, new PropertyBinder(name, propertyChanged, mode));
|
||||
}
|
||||
|
||||
private string GetPropertyName<T>(Expression<Func<T>> predicate)
|
||||
{
|
||||
if (predicate is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
}
|
||||
|
||||
Expression? body = predicate.Body;
|
||||
MemberExpression? memberExpression = body as MemberExpression ?? (MemberExpression)((UnaryExpression)body).Operand;
|
||||
return memberExpression.Member.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public enum PropertyChangedMode
|
||||
{
|
||||
Default,
|
||||
Explicit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class PropertyValidation
|
||||
{
|
||||
public PropertyValidation(Func<bool> validation)
|
||||
{
|
||||
Validation = validation;
|
||||
}
|
||||
|
||||
public PropertyValidation(Func<bool> validation, string message)
|
||||
{
|
||||
Validation = validation;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public string? Message { get; }
|
||||
|
||||
public Func<bool>? Validation { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TheXamlGuy.Framework.Core
|
||||
{
|
||||
public class PropertyValidationError<TKey, TValue> where TKey : class where TValue : class
|
||||
{
|
||||
private readonly Dictionary<TKey, TValue?> dictionary = new();
|
||||
|
||||
public int Count => dictionary.Count;
|
||||
|
||||
public TValue? this[[MaybeNull]TKey key]
|
||||
{
|
||||
get => dictionary.ContainsKey(key) ? dictionary[key] : null;
|
||||
set
|
||||
{
|
||||
if (Contains(key))
|
||||
{
|
||||
dictionary.Remove(key);
|
||||
}
|
||||
|
||||
dictionary.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(TKey key)
|
||||
{
|
||||
return dictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
public void Remove(TKey key)
|
||||
{
|
||||
if (Contains(key))
|
||||
{
|
||||
dictionary.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user