diff --git a/docs/blazor/admin.md b/docs/blazor/admin.md index feec095..e89a8f4 100644 --- a/docs/blazor/admin.md +++ b/docs/blazor/admin.md @@ -25,6 +25,8 @@ simply by reading the structure of the provided model and optionally some additi } ``` + > **Hint:** you can specify the url of the admin page by adding the `AdminPageUrl` Attribute + 3. **Optionally** you can further configure your pages in the `OnModelCreating` method ```csharp @@ -61,6 +63,15 @@ simply by reading the structure of the provided model and optionally some additi ``` 4. **Optionally** you can also add some of the following attributes to your classes / properties to further configure the admin pages:\ \ + Attributes for classes and properties: + + ```csharp + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] + public sealed class AdminNameAttribute(string name) : Attribute { + public string Name { get; set; } = name; + } + ``` + Attributes for classes: ```csharp @@ -86,8 +97,8 @@ simply by reading the structure of the provided model and optionally some additi ```csharp [AttributeUsage(AttributeTargets.Class)] - public class AdminUrlAttribute(string url) : Attribute { - public string Url { get; set; } = url; + public sealed class AdminDescriptionAttribute(string description) : Attribute { + public string Description { get; set; } = description; } ``` diff --git a/src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs b/src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs new file mode 100644 index 0000000..ddc49df --- /dev/null +++ b/src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs @@ -0,0 +1,10 @@ +namespace HopFrame.Web.Admin.Attributes; + +/// +/// This attribute specifies the url of the admin page and needs to be applied on the AdminPage property in the AdminContext directly +/// +/// The page url: '/administration/{url}' +[AttributeUsage(AttributeTargets.Property)] +public sealed class AdminPageUrlAttribute(string url) : Attribute { + public string Url { get; set; } = url; +} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminDescriptionAttribute.cs similarity index 72% rename from src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs rename to src/HopFrame.Web.Admin/Attributes/Classes/AdminDescriptionAttribute.cs index 92e3fe3..cc23438 100644 --- a/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs +++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminDescriptionAttribute.cs @@ -1,6 +1,6 @@ namespace HopFrame.Web.Admin.Attributes; -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] +[AttributeUsage(AttributeTargets.Class)] public sealed class AdminDescriptionAttribute(string description) : Attribute { public string Description { get; set; } = description; } \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminUrlAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminUrlAttribute.cs deleted file mode 100644 index ab87a2e..0000000 --- a/src/HopFrame.Web.Admin/Attributes/Classes/AdminUrlAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace HopFrame.Web.Admin.Attributes.Classes; - -[AttributeUsage(AttributeTargets.Class)] -public class AdminUrlAttribute(string url) : Attribute { - public string Url { get; set; } = url; -} \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs index 2a71a75..30a6ef5 100644 --- a/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs @@ -18,13 +18,6 @@ public interface IAdminPageGenerator { /// the specified description /// IAdminPageGenerator Description(string description); - - /// - /// Sets the url for the Admin Page - /// - /// the specified url (administration/{url}) - /// - IAdminPageGenerator Url(string url); /// /// Sets the permission needed to view the Admin Page diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs index ae62af6..54137d4 100644 --- a/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs @@ -1,6 +1,5 @@ +using HopFrame.Web.Admin.Attributes; using HopFrame.Web.Admin.Models; -using HopFrame.Web.Admin.Providers; -using Microsoft.Extensions.DependencyInjection; namespace HopFrame.Web.Admin.Generators.Implementation; @@ -27,13 +26,14 @@ internal class AdminContextGenerator : IAdminContextGenerator { return (generator as AdminPageGenerator)?.Compile(); } - public TContext CompileContext() where TContext : AdminPagesContext { + public TContext CompileContext(IServiceProvider provider) where TContext : AdminPagesContext { var type = typeof(TContext); var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage)); var properties = type.GetProperties(); - var context = Activator.CreateInstance(); + var dependencies = ResolveDependencies(provider); + var context = Activator.CreateInstance(type, dependencies) as TContext; foreach (var property in properties) { var propertyType = property.PropertyType.GenericTypeArguments[0]; @@ -49,31 +49,48 @@ internal class AdminContextGenerator : IAdminContextGenerator { _adminPages.Add(propertyType, generatorInstance); } - context.OnModelCreating(this); + context?.OnModelCreating(this); foreach (var property in properties) { var modelType = property.PropertyType.GenericTypeArguments[0]; var method = compileMethod?.MakeGenericMethod(modelType); - property.SetValue(context, method?.Invoke(this, [])); + var compiledPage = method?.Invoke(this, []) as AdminPage; + + var url = property.Name; + if (property.GetCustomAttributes(false).Any(a => a is AdminPageUrlAttribute)) { + var attribute = property.GetCustomAttributes(false) + .Single(a => a is AdminPageUrlAttribute) as AdminPageUrlAttribute; + + url = attribute?.Url; + } + compiledPage!.Url = url; + + property.SetValue(context, compiledPage); } return context; } + private object[] ResolveDependencies(IServiceProvider provider) { + return ResolveDependencies(typeof(TContext), provider); + } + + public static object[] ResolveDependencies(Type type, IServiceProvider provider) { + var ctors = type.GetConstructors(); + if (ctors.Length == 0) return []; + if (ctors.Length > 1) + throw new ArgumentException($"Dependencies of {type.Name} could not be resolved (multiple constructors)!"); - public static void RegisterPages(AdminPagesContext context, IAdminPagesProvider provider, IServiceCollection services) { - var properties = context.GetType().GetProperties(); - - foreach (var property in properties) { - var page = property.GetValue(context) as AdminPage; - if (page is null) continue; - - provider.RegisterAdminPage(page.Title.ToLower(), page); - - if (page.RepositoryProvider is not null) - services.AddScoped(page.RepositoryProvider); + var ctor = ctors[0]; + var depTypes = ctor.GetParameters(); + var dependencies = new object[depTypes.Length]; + + for (var i = 0; i < depTypes.Length; i++) { + dependencies[i] = provider.GetService(depTypes[i].ParameterType); } + + return dependencies; } } \ No newline at end of file diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs index 2718e15..d2fb6ec 100644 --- a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs @@ -40,7 +40,6 @@ internal sealed class AdminPageGenerator : IAdminPageGenerator, public IAdminPageGenerator Title(string title) { Page.Title = title; - Page.Url ??= title.ToLower(); return this; } @@ -49,11 +48,6 @@ internal sealed class AdminPageGenerator : IAdminPageGenerator, return this; } - public IAdminPageGenerator Url(string url) { - Page.Url = url; - return this; - } - public IAdminPageGenerator ViewPermission(string permission) { Page.Permissions.View = permission; return this; @@ -167,11 +161,6 @@ internal sealed class AdminPageGenerator : IAdminPageGenerator, Description(attribute?.Description); } - if (attributes.Any(a => a is AdminUrlAttribute)) { - var attribute = attributes.Single(a => a is AdminUrlAttribute) as AdminUrlAttribute; - Url(attribute?.Url); - } - if (attributes.Any(a => a is AdminPermissionsAttribute)) { var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute; CreatePermission(attribute?.Permissions.Create); diff --git a/src/HopFrame.Web.Admin/Models/AdminPage.cs b/src/HopFrame.Web.Admin/Models/AdminPage.cs index 748eb00..1eb43db 100644 --- a/src/HopFrame.Web.Admin/Models/AdminPage.cs +++ b/src/HopFrame.Web.Admin/Models/AdminPage.cs @@ -1,5 +1,5 @@ using System.ComponentModel; -using System.Text.Json.Serialization; +using HopFrame.Web.Admin.Generators.Implementation; namespace HopFrame.Web.Admin.Models; @@ -23,4 +23,11 @@ public class AdminPage { public bool ShowCreateButton { get; set; } = true; public bool ShowDeleteButton { get; set; } = true; public bool ShowUpdateButton { get; set; } = true; + + public IModelRepository LoadModelRepository(IServiceProvider provider) { + if (RepositoryProvider is null) return null; + + var dependencies = AdminContextGenerator.ResolveDependencies(RepositoryProvider, provider); + return Activator.CreateInstance(RepositoryProvider, dependencies) as IModelRepository; + } } diff --git a/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs b/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs index 564f52a..34b5a4d 100644 --- a/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs +++ b/src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs @@ -4,7 +4,6 @@ namespace HopFrame.Web.Admin.Providers; public interface IAdminPagesProvider { - internal void RegisterAdminPage(string url, AdminPage page); AdminPage LoadAdminPage(string url); IList LoadRegisteredAdminPages(); AdminPage HasPageFor(Type type); diff --git a/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs b/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs index b2e38b4..223e5e2 100644 --- a/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs +++ b/src/HopFrame.Web.Admin/Providers/Implementation/AdminPagesProvider.cs @@ -2,25 +2,44 @@ using HopFrame.Web.Admin.Models; namespace HopFrame.Web.Admin.Providers.Implementation; -public class AdminPagesProvider : IAdminPagesProvider { - private readonly IDictionary _pages = new Dictionary(); - - public void RegisterAdminPage(string url, AdminPage page) { - _pages.Add(url, page); +public class AdminPagesProvider(IServiceProvider provider) : IAdminPagesProvider { + private static readonly IDictionary Pages = new Dictionary(); + + public static void RegisterAdminPage(string url, Type pageType) where TContext : AdminPagesContext { + Pages.Add(url, new PageDataStore { + ContextType = typeof(TContext), + PageType = pageType + }); } public AdminPage LoadAdminPage(string url) { - return _pages.TryGetValue(url, out var page) ? page : null; + if (!Pages.TryGetValue(url, out var data)) return null; + + var context = provider.GetService(data.ContextType); + var property = data.ContextType.GetProperties() + .SingleOrDefault(prop => prop.PropertyType == data.PageType); + + return property?.GetValue(context) as AdminPage; } public IList LoadRegisteredAdminPages() { - return _pages.Values.ToList(); + return Pages + .Select(pair => LoadAdminPage(pair.Key)) + .ToList(); } public AdminPage HasPageFor(Type type) { - return _pages - .Where(p => p.Value.ModelType == type) - .Select(p => p.Value) - .SingleOrDefault(); + foreach (var (url, data) in Pages) { + var innerType = data.PageType.GenericTypeArguments[0]; + if (innerType != type) continue; + return LoadAdminPage(url); + } + + return null; } -} \ No newline at end of file +} + +internal struct PageDataStore { + public Type PageType { get; set; } + public Type ContextType { get; set; } +} diff --git a/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs b/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs index f3e8370..87aaf32 100644 --- a/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using HopFrame.Web.Admin.Attributes; using HopFrame.Web.Admin.Generators.Implementation; using HopFrame.Web.Admin.Providers; using HopFrame.Web.Admin.Providers.Implementation; @@ -8,22 +9,36 @@ namespace HopFrame.Web.Admin; public static class ServiceCollectionExtensions { - private static IAdminPagesProvider _provider; - public static IServiceCollection AddAdminContext(this IServiceCollection services) where TContext : AdminPagesContext { - var provider = GetProvider(); - services.TryAddSingleton(provider); + services.TryAddSingleton(); - var generator = new AdminContextGenerator(); - var context = generator.CompileContext(); - AdminContextGenerator.RegisterPages(context, provider, services); - services.AddSingleton(context); + services.AddSingleton(provider => { + var generator = new AdminContextGenerator(); + var context = generator.CompileContext(provider); + return context; + }); + + PreregisterPages(); return services; } - private static IAdminPagesProvider GetProvider() { - return _provider ??= new AdminPagesProvider(); + private static void PreregisterPages() where TContext : AdminPagesContext { + var contextType = typeof(TContext); + var props = contextType.GetProperties(); + + foreach (var property in props) { + var url = property.Name; + + if (property.GetCustomAttributes(false).Any(a => a is AdminPageUrlAttribute)) { + var attribute = property.GetCustomAttributes(false) + .Single(a => a is AdminPageUrlAttribute) as AdminPageUrlAttribute; + + url = attribute?.Url; + } + + AdminPagesProvider.RegisterAdminPage(url, property.PropertyType); + } } } \ No newline at end of file diff --git a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor index f410739..8e04608 100644 --- a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor +++ b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor @@ -1,7 +1,6 @@ @rendermode InteractiveServer @using System.Collections -@using System.Globalization @using BlazorStrap @using BlazorStrap.Shared.Components.Modal @using static Microsoft.AspNetCore.Components.Web.RenderMode @@ -144,7 +143,7 @@ _currentPage = page; _entry = entryToEdit; _isEdit = entryToEdit is not null; - _repository = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository; + _repository = _currentPage.LoadModelRepository(Provider); _entry ??= Activator.CreateInstance(_currentPage.ModelType); _context = new EditContext(_entry); @@ -247,8 +246,7 @@ foreach (var value in _values) { if (value.Key.Unique) { if (value.Value == value.Key.GetValue(_entry)) continue; - var repo = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository; - var data = repo!.ReadAllO().GetAwaiter().GetResult(); + var data = _repository!.ReadAllO().GetAwaiter().GetResult(); foreach (var entry in data) { var other = value.Key.GetValue(entry); if (!other.Equals(value.Value)) continue; @@ -295,7 +293,7 @@ throw new ArgumentException($"'{property.Name}' cannot be a selector because a admin page for '{type.Name}' does not exist!"); } - var repo = Provider.GetService(page.RepositoryProvider) as IModelRepository; + var repo = page.LoadModelRepository(Provider); var objects = (await repo!.ReadAllO()).ToArray(); _selectorValues[property] = objects; diff --git a/src/HopFrame.Web/HopAdminContext.cs b/src/HopFrame.Web/HopAdminContext.cs index 22aac7b..6d89e76 100644 --- a/src/HopFrame.Web/HopAdminContext.cs +++ b/src/HopFrame.Web/HopAdminContext.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using HopFrame.Database.Models; using HopFrame.Security; using HopFrame.Web.Admin; +using HopFrame.Web.Admin.Attributes; using HopFrame.Web.Admin.Generators; using HopFrame.Web.Admin.Models; using HopFrame.Web.Repositories; @@ -10,7 +11,10 @@ namespace HopFrame.Web; internal class HopAdminContext : AdminPagesContext { + [AdminPageUrl("users")] public AdminPage Users { get; set; } + + [AdminPageUrl("groups")] public AdminPage Groups { get; set; } public override void OnModelCreating(IAdminContextGenerator generator) { diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor index cba8f4f..2f2519c 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor @@ -106,6 +106,7 @@ @inject ITokenContext Auth @inject IPermissionRepository Permissions @inject SweetAlertService Alerts +@inject NavigationManager Navigator @code { [Parameter] @@ -127,12 +128,17 @@ protected override async Task OnInitializedAsync() { _pageData = Pages.LoadAdminPage(Url); + if (_pageData is null) { + Navigator.NavigateTo("/administration", true); + return; + } + _currentSortProperty = _pageData.DefaultSortPropertyName; _currentSortDirection = _pageData.DefaultSortDirection; if (_pageData.RepositoryProvider is null) throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'"); - _modelRepository = Provider.GetService(_pageData.RepositoryProvider) as IModelRepository; + _modelRepository = _pageData.LoadModelRepository(Provider); _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update); _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);