diff --git a/.gitignore b/.gitignore index c0c35c1..fbf55e3 100644 --- a/.gitignore +++ b/.gitignore @@ -423,6 +423,7 @@ FodyWeavers.xsd !/build !/docs !/examples +!/dependencies !/src !/tests !/tools diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6f752c3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/Connected"] + path = dependencies/Connected + url = https://git.tompit.com/Connected/Connected.git diff --git a/Connected.UI.sln b/Connected.UI.sln new file mode 100644 index 0000000..3a6e345 --- /dev/null +++ b/Connected.UI.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.32916.344 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected", "src\Connected\Connected.csproj", "{26AD2745-4DB4-4603-A944-C54D1A374032}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Client", "src\Connected.Client\Connected.Client.csproj", "{F0E0F91D-DFCB-44B7-8BA1-5E74C5E008F5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.UI", "src\Connected.UI\Connected.UI.csproj", "{52D89174-140B-4017-8B52-B83FA936D809}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{0A1EBC0F-0567-41FB-9187-7DE1722FAB4E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Components", "..\connected.components\src\Connected.Components\Connected.Components.csproj", "{7A6A8DFF-E5B4-482D-98F9-B2AE76235093}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {26AD2745-4DB4-4603-A944-C54D1A374032}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26AD2745-4DB4-4603-A944-C54D1A374032}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26AD2745-4DB4-4603-A944-C54D1A374032}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26AD2745-4DB4-4603-A944-C54D1A374032}.Release|Any CPU.Build.0 = Release|Any CPU + {F0E0F91D-DFCB-44B7-8BA1-5E74C5E008F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0E0F91D-DFCB-44B7-8BA1-5E74C5E008F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0E0F91D-DFCB-44B7-8BA1-5E74C5E008F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0E0F91D-DFCB-44B7-8BA1-5E74C5E008F5}.Release|Any CPU.Build.0 = Release|Any CPU + {52D89174-140B-4017-8B52-B83FA936D809}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52D89174-140B-4017-8B52-B83FA936D809}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52D89174-140B-4017-8B52-B83FA936D809}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52D89174-140B-4017-8B52-B83FA936D809}.Release|Any CPU.Build.0 = Release|Any CPU + {7A6A8DFF-E5B4-482D-98F9-B2AE76235093}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A6A8DFF-E5B4-482D-98F9-B2AE76235093}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A6A8DFF-E5B4-482D-98F9-B2AE76235093}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A6A8DFF-E5B4-482D-98F9-B2AE76235093}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7A6A8DFF-E5B4-482D-98F9-B2AE76235093} = {0A1EBC0F-0567-41FB-9187-7DE1722FAB4E} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4BE29712-DF74-4798-8F2E-92D7446D2465} + EndGlobalSection +EndGlobal diff --git a/dependencies/Connected b/dependencies/Connected new file mode 160000 index 0000000..5721864 --- /dev/null +++ b/dependencies/Connected @@ -0,0 +1 @@ +Subproject commit 5721864f19815bd8f906ce52ae9f74301db69657 diff --git a/src/Connected.UI/Connected.UI.csproj b/src/Connected.UI/Connected.UI.csproj new file mode 100644 index 0000000..e99c91c --- /dev/null +++ b/src/Connected.UI/Connected.UI.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/Connected.UI/Instance.cs b/src/Connected.UI/Instance.cs new file mode 100644 index 0000000..21f2f9e --- /dev/null +++ b/src/Connected.UI/Instance.cs @@ -0,0 +1,63 @@ +using Connected.Middleware; +using Connected.Startup; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Connected; + +public static class Instance +{ + public static WebAssemblyHost Host { get; private set; } = default!; + private static IServiceProvider ServiceProvider { get; set; } = default!; + + internal static T? GetService() + { + return ServiceProvider.GetService(); + } + public static async Task Start(List startups, Dictionary args) where TApp : IComponent + { + var builder = WebAssemblyHostBuilder.CreateDefault(UnpackArguments(args)); + + builder.RootComponents.Add("#app"); + builder.RootComponents.Add("head::after"); + + builder.Services.AddHttpClient(); + + builder.Services.AddSingleton(typeof(IComponentMiddlewareService), typeof(ComponentMiddlewareService)); + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + foreach (var startup in startups) + await startup.ConfigureServices(builder.Services); + + Host = builder.Build(); + + foreach (var startup in startups) + await startup.Configure(Host); + + await Host.RunAsync(); + } + + private static string[] UnpackArguments(Dictionary args) + { + var result = new string[args.Count]; + + for (var i = 0; i < args.Count; i++) + { + var arg = args.ElementAt(i); + + result[i] = string.IsNullOrWhiteSpace(arg.Value) ? arg.Key : $"{arg.Key}={arg.Value}"; + } + + return result; + } + + public static void UseMiddlewareComponent(this WebAssemblyHost app) + { + if (app.Services.GetService() is not IComponentMiddlewareService service) + throw new NullReferenceException(nameof(IComponentMiddlewareService)); + + service.Add(); + } +} \ No newline at end of file diff --git a/src/Connected.UI/Layouts/DefaultLayout.razor b/src/Connected.UI/Layouts/DefaultLayout.razor new file mode 100644 index 0000000..2fce8ce --- /dev/null +++ b/src/Connected.UI/Layouts/DefaultLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + + + + + + + +@Body \ No newline at end of file diff --git a/src/Connected.UI/Middleware/ComponentMiddlewareService.cs b/src/Connected.UI/Middleware/ComponentMiddlewareService.cs new file mode 100644 index 0000000..902d30a --- /dev/null +++ b/src/Connected.UI/Middleware/ComponentMiddlewareService.cs @@ -0,0 +1,42 @@ +using System.Collections.Concurrent; + +namespace Connected.Middleware; + +internal class ComponentMiddlewareService : IComponentMiddlewareService +{ + public ComponentMiddlewareService() + { + Middleware = new(); + } + + private ConcurrentDictionary> Middleware { get; } + + public void Add() + { + var componentName = typeof(TComponent).FullName; + + if (string.IsNullOrEmpty(componentName)) + throw new ArgumentException(null, nameof(TMiddleware)); + + if (Middleware.TryGetValue(componentName, out List? items)) + { + //TODO: sort by priority so multiple inheritance would work + items.Add(typeof(TMiddleware)); + } + else + Middleware.TryAdd(componentName, new List { typeof(TMiddleware) }); + } + + public Type? Select() + { + var typeName = typeof(TComponent).FullName; + + if (string.IsNullOrEmpty(typeName)) + throw new ArgumentException(null, nameof(TComponent)); + + if (Middleware.TryGetValue(typeName, out List? items) && items is not null && items.Any()) + return items[0]; + + return null; + } +} diff --git a/src/Connected.UI/Middleware/IComponentMiddlewareService.cs b/src/Connected.UI/Middleware/IComponentMiddlewareService.cs new file mode 100644 index 0000000..4b347b3 --- /dev/null +++ b/src/Connected.UI/Middleware/IComponentMiddlewareService.cs @@ -0,0 +1,8 @@ +namespace Connected.Middleware; + +public interface IComponentMiddlewareService +{ + void Add(); + + Type? Select(); +} diff --git a/src/Connected.UI/UIComposition.cs b/src/Connected.UI/UIComposition.cs new file mode 100644 index 0000000..a48d635 --- /dev/null +++ b/src/Connected.UI/UIComposition.cs @@ -0,0 +1,29 @@ +using Connected.Middleware; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Logging; + +namespace Connected.Components; + +public abstract class UIComposition : ComponentBase +{ + private ILogger _logger; + + [Inject] + private ILoggerFactory LoggerFactory { get; set; } = default!; + + [Inject] + private IComponentMiddlewareService? MiddlewareService { get; set; } + + protected ILogger Logger => _logger ??= LoggerFactory.CreateLogger(GetType()); + + protected Type ResolveComponent() + { + if (MiddlewareService is null) + return typeof(TComponent); + + if (MiddlewareService.Select() is Type type) + return type; + + return typeof(TComponent); + } +}