Changed CascadingDependencies to be more modular.

This commit is contained in:
Harrison Deng 2021-06-02 14:41:32 -05:00
parent d57a61d5ca
commit 3957d65370
10 changed files with 174 additions and 65 deletions

View File

@ -1,5 +1,5 @@
<CascadingAuthenticationState> <CascadingAuthenticationState>
<CascadingDependencies> <CascadingDependencies Dependencies="@dependencies">
<Content> <Content>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData"> <Found Context="routeData">

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using MultiShop.Client.Module;
using MultiShop.Client.Shared;
using MultiShop.Shared.Models;
using MultiShop.Shop.Framework;
namespace MultiShop.Client
{
public partial class App
{
private ICollection<RuntimeDependencyManager.Dependency> dependencies = new List<RuntimeDependencyManager.Dependency>();
protected override void OnInitialized()
{
base.OnInitialized();
dependencies.Add(new RuntimeDependencyManager.Dependency(typeof(IReadOnlyDictionary<string, IShop>), "Shops", DownloadShops));
dependencies.Add(new RuntimeDependencyManager.Dependency(typeof(ApplicationProfile), "Application Profile", DownloadApplicationProfile));
}
private async ValueTask<object> DownloadShops(HttpClient publicHttp, HttpClient http, AuthenticationState authState, ILogger logger)
{
ShopModuleLoader loader = new ShopModuleLoader(publicHttp, logger);
return await loader.GetShops();
}
private async ValueTask<object> DownloadApplicationProfile(HttpClient publicHttp, HttpClient http, AuthenticationState authState, ILogger logger)
{
if (authState.User.Identity.IsAuthenticated)
{
logger.LogDebug($"User is logged in. Attempting to fetch application profile.");
HttpResponseMessage response = await http.GetAsync("Profile/Application");
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<ApplicationProfile>();
}
}
return new ApplicationProfile();
}
}
}

View File

@ -25,7 +25,9 @@ namespace MultiShop.Client.Pages
[CascadingParameter] [CascadingParameter]
private Task<AuthenticationState> AuthenticationStateTask { get; set; } private Task<AuthenticationState> AuthenticationStateTask { get; set; }
[CascadingParameter(Name = "ApplicationProfile")] [CascadingParameter(Name = "RuntimeDependencyManager")]
private RuntimeDependencyManager RuntimeDependencyManager { get; set; }
private ApplicationProfile ApplicationProfile { get; set; } private ApplicationProfile ApplicationProfile { get; set; }
private bool collapseNavMenu; private bool collapseNavMenu;
@ -50,6 +52,12 @@ namespace MultiShop.Client.Pages
Section.Search Section.Search
}; };
protected override void OnInitialized()
{
base.OnInitialized();
ApplicationProfile = RuntimeDependencyManager.Get<ApplicationProfile>();
}
private string GetNavItemCssClass(Section section) private string GetNavItemCssClass(Section section)
{ {
return "nav-item" + ((section == currentSection) ? " active" : null); return "nav-item" + ((section == currentSection) ? " active" : null);

View File

@ -29,8 +29,11 @@ namespace MultiShop.Client.Pages
[CascadingParameter] [CascadingParameter]
private Task<AuthenticationState> AuthenticationStateTask { get; set; } private Task<AuthenticationState> AuthenticationStateTask { get; set; }
[CascadingParameter(Name = "Shops")] [CascadingParameter(Name = "RuntimeDependencyManager")]
public IReadOnlyDictionary<string, IShop> Shops { get; set; } public RuntimeDependencyManager RuntimeDependencyManager { get; set; }
private IReadOnlyDictionary<string, IShop> Shops { get; set; }
[Parameter] [Parameter]
public string Query { get; set; } public string Query { get; set; }
@ -51,6 +54,7 @@ namespace MultiShop.Client.Pages
{ {
base.OnInitialized(); base.OnInitialized();
LayoutStateChangeNotifier.Notify += UpdateState; LayoutStateChangeNotifier.Notify += UpdateState;
Shops = RuntimeDependencyManager.Get<IReadOnlyDictionary<string, IShop>>();
} }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
namespace MultiShop.Client
{
public class RuntimeDependencyManager : IAsyncDisposable
{
private bool disposedValue;
private HttpClient authenticatedHttp;
private HttpClient publicHttp;
private Task<AuthenticationState> authenticationStateTask;
private ILogger logger;
private Dictionary<Type, Dictionary<string, object>> RuntimeLoadedDependencies = new Dictionary<Type, Dictionary<string, object>>();
public RuntimeDependencyManager(HttpClient publicHttp, HttpClient authenticatedHttp, Task<AuthenticationState> authenticationStateTask, ILogger logger)
{
this.publicHttp = publicHttp;
this.authenticatedHttp = authenticatedHttp;
this.authenticationStateTask = authenticationStateTask;
this.logger = logger;
}
public async ValueTask SetupDependency(Dependency dependency)
{
logger.LogDebug($"Setting up dependency of type \"{dependency.Type}\" named \"{dependency.Name}\".");
Dictionary<string, object> dependencies = RuntimeLoadedDependencies.GetValueOrDefault(dependency.Type, new Dictionary<string, object>());
dependencies.Add(dependency.Name, await dependency.LoadDependency.Invoke(publicHttp, authenticatedHttp, await authenticationStateTask, logger));
RuntimeLoadedDependencies[dependency.Type] = dependencies;
}
public T Get<T>(string name = "") {
Type type = typeof(T);
if (!RuntimeLoadedDependencies.ContainsKey(typeof(T))) throw new InvalidOperationException($"No dependency of type {type}.");
if (!RuntimeLoadedDependencies[type].ContainsKey(name)) throw new InvalidOperationException($"No dependency of type {type} with name {name}.");
return (T) RuntimeLoadedDependencies[type][name];
}
public async ValueTask DisposeAsync()
{
if (!disposedValue) {
foreach (Dictionary<string, object> dependencies in RuntimeLoadedDependencies.Values)
{
foreach (object dependency in dependencies.Values)
{
IDisposable disposableDep = dependency as IDisposable;
if (disposableDep != null) {
disposableDep.Dispose();
} else {
IAsyncDisposable asyncDisposableDep = dependency as IAsyncDisposable;
if (asyncDisposableDep != null) {
await asyncDisposableDep.DisposeAsync();
}
}
}
}
}
RuntimeLoadedDependencies.Clear();
disposedValue = true;
}
public class Dependency
{
public Type Type { get; }
public string Name { get; }
public string DisplayName { get; }
public Func<HttpClient, HttpClient, AuthenticationState, ILogger, ValueTask<object>> LoadDependency { get; }
public Dependency(Type type, string displayName, Func<HttpClient, HttpClient, AuthenticationState, ILogger, ValueTask<object>> LoadDependencyFunc, string name = null)
{
this.Type = type;
this.DisplayName = displayName;
this.Name = name ?? "";
this.LoadDependency = LoadDependencyFunc;
}
}
}
}

View File

@ -4,10 +4,8 @@
@if (loadingDisplay == null) @if (loadingDisplay == null)
{ {
<CascadingValue Value="@applicationProfile" Name="ApplicationProfile"> <CascadingValue Value="@manager" Name="RuntimeDependencyManager">
<CascadingValue Value="@shops" Name="Shops"> @Content
@Content
</CascadingValue>
</CascadingValue> </CascadingValue>
} else { } else {
@LoadingContent(loadingDisplay) @LoadingContent(loadingDisplay)

View File

@ -1,18 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MultiShop.Client.Module;
using MultiShop.Shared.Models; using MultiShop.Shared.Models;
using MultiShop.Shop.Framework; using MultiShop.Shop.Framework;
namespace MultiShop.Client.Shared namespace MultiShop.Client.Shared
{ {
public partial class CascadingDependencies : IDisposable public partial class CascadingDependencies : IAsyncDisposable
{ {
[Inject] [Inject]
private ILogger<CascadingDependencies> Logger { get; set; } private ILogger<CascadingDependencies> Logger { get; set; }
@ -29,65 +27,36 @@ namespace MultiShop.Client.Shared
[Parameter] [Parameter]
public RenderFragment Content { get; set; } public RenderFragment Content { get; set; }
[Parameter]
public ICollection<RuntimeDependencyManager.Dependency> Dependencies { get; set; }
private RuntimeDependencyManager manager;
private bool disposedValue; private bool disposedValue;
private string loadingDisplay; private string loadingDisplay;
private IReadOnlyDictionary<string, IShop> shops;
private ApplicationProfile applicationProfile;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
loadingDisplay = ""; loadingDisplay = "stuff";
await base.OnInitializedAsync(); await base.OnInitializedAsync();
await DownloadShops(HttpClientFactory.CreateClient("Public-MultiShop.ServerAPI")); manager = new RuntimeDependencyManager(HttpClientFactory.CreateClient("Public-MultiShop.ServerAPI"), HttpClientFactory.CreateClient("MultiShop.ServerAPI"), AuthenticationStateTask, Logger);
await DownloadApplicationProfile(HttpClientFactory.CreateClient("MultiShop.ServerAPI")); foreach (RuntimeDependencyManager.Dependency dep in Dependencies)
{
loadingDisplay = dep.DisplayName;
await manager.SetupDependency(dep);
}
loadingDisplay = null; loadingDisplay = null;
} }
private async Task DownloadShops(HttpClient http)
{
loadingDisplay = "shops";
ShopModuleLoader loader = new ShopModuleLoader(http, Logger);
shops = await loader.GetShops();
}
private async Task DownloadApplicationProfile(HttpClient http)
public async ValueTask DisposeAsync()
{ {
loadingDisplay = "profile"; if (!disposedValue) {
AuthenticationState authState = await AuthenticationStateTask; await manager.DisposeAsync();
if (authState.User.Identity.IsAuthenticated)
{
Logger.LogDebug($"User is logged in. Attempting to fetch application profile.");
HttpResponseMessage response = await http.GetAsync("Profile/Application");
if (response.IsSuccessStatusCode)
{
applicationProfile = await response.Content.ReadFromJsonAsync<ApplicationProfile>();
}
} }
if (applicationProfile == null) applicationProfile = new ApplicationProfile(); disposedValue = true;
} }
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
foreach (string shopName in shops.Keys)
{
shops[shopName].Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
} }
} }

View File

@ -4,7 +4,7 @@
@implements IDisposable @implements IDisposable
@inject LayoutStateChangeNotifier LayoutStateChangeNotifier @inject LayoutStateChangeNotifier LayoutStateChangeNotifier
<nav class=@ApplicationProfile.GetNavCssClass("navbar navbar-expand-lg")> <nav class=@(RuntimeDependencyManager.Get<ApplicationProfile>().GetNavCssClass("navbar navbar-expand-lg"))>
<a class="navbar-brand" href=""> <a class="navbar-brand" href="">
@BrandContent @BrandContent
</a> </a>
@ -14,7 +14,7 @@
<div class=@NavMenuCssClass> <div class=@NavMenuCssClass>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
@foreach (string item in Items) @foreach (string item in Places)
{ {
<li class="nav-item"> <li class="nav-item">
<NavLink class="nav-link" href=@item Match=@(string.IsNullOrEmpty(item) ? NavLinkMatch.All : NavLinkMatch.Prefix)> <NavLink class="nav-link" href=@item Match=@(string.IsNullOrEmpty(item) ? NavLinkMatch.All : NavLinkMatch.Prefix)>
@ -28,15 +28,15 @@
</nav> </nav>
@code { @code {
[CascadingParameter(Name = "ApplicationProfile")] [CascadingParameter(Name = "RuntimeDependencyManager")]
private ApplicationProfile ApplicationProfile { get; set; } private RuntimeDependencyManager RuntimeDependencyManager { get; set; }
private bool collapseNavMenu = true; private bool collapseNavMenu = true;
private string NavMenuCssClass => (collapseNavMenu ? "collapse " : " ") + "navbar-collapse"; private string NavMenuCssClass => (collapseNavMenu ? "collapse " : " ") + "navbar-collapse";
[Parameter] [Parameter]
public IList<string> Items { get; set; } public IList<string> Places { get; set; }
[Parameter] [Parameter]
public RenderFragment BrandContent { get; set; } public RenderFragment BrandContent { get; set; }

View File

@ -5,7 +5,7 @@
@inject SignOutSessionStateManager SignOutManager @inject SignOutSessionStateManager SignOutManager
@inject IConfiguration Configuration @inject IConfiguration Configuration
<HorizontalNavMenuTemplate Items=@PlaceOrder> <HorizontalNavMenuTemplate Places=@PlaceOrder>
<BrandContent> <BrandContent>
<img src="images/100x100.png" width="30" height="30" class="d-inline-block align-top"> <img src="images/100x100.png" width="30" height="30" class="d-inline-block align-top">
MultiShop MultiShop

View File

@ -5,7 +5,7 @@
@implements IDisposable @implements IDisposable
@inject LayoutStateChangeNotifier LayoutStateChangeNotifier @inject LayoutStateChangeNotifier LayoutStateChangeNotifier
<div class=@ApplicationProfile.GetPageCssClass("page")> <div class=@(dependencyManager.Get<ApplicationProfile>("").GetPageCssClass("page"))>
<HorizontalSiteNav /> <HorizontalSiteNav />
<div class="content"> <div class="content">
@Body @Body
@ -13,14 +13,16 @@
</div> </div>
@code { @code {
[CascadingParameter(Name = "ApplicationProfile")] [CascadingParameter(Name = "RuntimeDependencyManager")]
private ApplicationProfile ApplicationProfile { get; set; } private RuntimeDependencyManager dependencyManager { get; set; }
private bool disposed; private bool disposed;
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
LayoutStateChangeNotifier.Notify += UpdateState; LayoutStateChangeNotifier.Notify += UpdateState;
} }
private async Task UpdateState() private async Task UpdateState()