Skip to content

Commit

Permalink
Implement #42
Browse files Browse the repository at this point in the history
- Ability to specify the module manually when using FullType
- Ability to pass arbitrary Parameters to the wrapped component
  • Loading branch information
isc30 committed May 6, 2020
1 parent eb4d45f commit 3be060c
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 61 deletions.
2 changes: 1 addition & 1 deletion demo/Demo.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{E503BF43
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LazyAreas", "LazyAreas", "{566D14F1-B2FB-463C-A627-C5EB23B763B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{DB1AE536-72C2-4CCD-A7D9-0A79AAEEDA54}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{DB1AE536-72C2-4CCD-A7D9-0A79AAEEDA54}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
11 changes: 11 additions & 0 deletions demo/Logger/Components/ComponentWithParameters.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@attribute [LazyName("Parametrized")]

<h3>Hello @Name, you are @Adjective</h3>

@code {
[Parameter]
public string Name { get; set; }

[Parameter]
public string Adjective { get; set; }
}
39 changes: 37 additions & 2 deletions demo/WasmHost/Pages/Counter.razor
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<LazyPlaceholder />
</Loading>
<Error>
<h2>Component loading errored...</h2>
<h2 style="color:red">Component loading errored...</h2>
</Error>
</Lazy>

Expand All @@ -23,6 +23,38 @@
</Loading>
</Lazy>

<Lazy
Name="Parametrized"
Parameters='new Dictionary<string, object> { { "Name", "Ivan" }, { "Adjective", "awesome" } }'
OnBeforeLoadAsync="Delay(500, 2500)">

<Loading>
<LazyPlaceholder />
</Loading>
<Error>
<h2 style="color:red">Component loading errored...</h2>
</Error>

</Lazy>

<Lazy Name="ErrorLol" Required="false" OnBeforeLoadAsync="Delay(500, 2500)">
<Loading>
<LazyPlaceholder />
</Loading>
<Error>
<h2 style="color:darkorange">(expected) Component loading errored...</h2>
</Error>
</Lazy>

<Lazy ModuleName="WasmHost" Name="WasmHost.Shared.SurveyPrompt" Required="true" OnBeforeLoadAsync="Delay(500, 2500)">
<Loading>
<LazyPlaceholder />
</Loading>
<Error>
<h2 style="color:red">Component loading errored...</h2>
</Error>
</Lazy>

@code {
private int currentCount = 0;

Expand All @@ -33,6 +65,9 @@

private Func<Lazy, Task> Delay(int minMs, int maxMs)
{
return (Lazy c) => Task.Delay(new Random().Next(minMs, maxMs));
return (Lazy c) =>
{
return Task.Delay(new Random().Next(minMs, maxMs));
};
}
}
196 changes: 138 additions & 58 deletions src/LazyComponents/LazyComponent/Lazy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
Expand All @@ -11,28 +12,62 @@

namespace BlazorLazyLoading
{
/// <summary>
/// Renders a Component (IComponent) from a Lazy Module based on it's 'LazyName' or 'TypeFullName'.
/// </summary>
public class Lazy : ComponentBase
{
[Parameter]
public string Name { get; set; } = null!;

[Parameter]
public bool Required { get; set; } = false;

[Parameter]
public RenderFragment? Loading { get; set; } = null;

[Parameter]
public RenderFragment? Error { get; set; } = null;

[Parameter]
public Func<Lazy, Task>? OnBeforeLoadAsync { get; set; } = null;

[Parameter]
public Action<Lazy>? OnAfterLoad { get; set; } = null;

/// <summary>
/// <br>Specifies the Component Name. This can be the 'LazyName' or the TypeFullName.</br>
/// <br>'LazyName' can be set using [LazyName] attribute on a external Component inside a module.</br>
/// </summary>
[Parameter] public string Name { get; set; } = null!;

/// <summary>
/// Specifies the list of parameters that will be passed to the Lazy Component.
/// </summary>
[Parameter] public IEnumerable<KeyValuePair<string, object>> Parameters { get; set; } = new Dictionary<string, object>();

/// <summary>
/// <br>Specifies if the Component is required (throws exceptions if load fails) or can error gracefully.</br>
/// <br>default: false</br>
/// </summary>
[Parameter] public bool Required { get; set; } = false;

/// <summary>
/// Specifies a custom 'Loading' view.
/// </summary>
[Parameter] public RenderFragment? Loading { get; set; } = null;

/// <summary>
/// Specifies a custom 'Error' view.
/// </summary>
[Parameter] public RenderFragment? Error { get; set; } = null;

/// <summary>
/// <br>This callback will be awaited before trying to resolve the Component from the manifests.</br>
/// <br>Useful for delaying a Component render and debugging with Task.Delay.</br>
/// </summary>
[Parameter] public Func<Lazy, Task>? OnBeforeLoadAsync { get; set; } = null;

/// <summary>
/// This callback will be invoked after resolving and rendering the Lazy Component.
/// </summary>
[Parameter] public Action<Lazy>? OnAfterLoad { get; set; } = null;

/// <summary>
/// Not recommended to use. Specifies a Module Name directly to avoid reading the manifests and uses Name as TypeFullName.
/// </summary>
[Parameter] public string? ModuleName { get; set; } = null;

/// <summary>
/// Exposes the resolved Type for the Lazy Component. Can be accessed from 'OnAfterLoad'.
/// </summary>
public Type? Type { get; protected set; } = null;

/// <summary>
/// Exposes the Instance the Lazy Component. Can be accessed from 'OnAfterLoad'.
/// </summary>
public IComponent? Instance { get; private set; } = null;

[Inject]
Expand All @@ -43,21 +78,100 @@ public class Lazy : ComponentBase

private RenderFragment? _currentFallbackBuilder = null;

/// <inheritdoc/>
public override async Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);

if (Name == null)
{
throw new InvalidOperationException($"The {nameof(Lazy)} component requires a value for the parameter {nameof(Name)}.");
}
}

/// <inheritdoc/>
protected override void OnInitialized()
{
_currentFallbackBuilder = Loading;
base.OnInitialized(); // trigger initial render
}

/// <inheritdoc/>
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync().ConfigureAwait(false);
try
{
await base.OnInitializedAsync().ConfigureAwait(false);

if (OnBeforeLoadAsync != null)
{
await OnBeforeLoadAsync(this);
}

string typeFullName = Name;

if (ModuleName == null)
{
var moduleInfo = await ResolveModuleAndType().ConfigureAwait(false);

if (moduleInfo == null)
{
DisplayErrorView(false);
return;
}

(ModuleName, typeFullName) = moduleInfo.Value;
}

Assembly? componentAssembly = await _assemblyLoader
.LoadAssemblyByNameAsync(new AssemblyName
{
Name = ModuleName,
Version = null,
})
.ConfigureAwait(false);

if (OnBeforeLoadAsync != null)
Type = componentAssembly?.GetType(typeFullName);

if (Type == null)
{
ThrowIfRequired($"Unable to load lazy component '{Name}'. Component type '{typeFullName}' not found in module '{ModuleName}'");
DisplayErrorView(false);
return;
}
}
catch (Exception ex)
{
await OnBeforeLoadAsync(this);
DisplayErrorView(false);
ThrowIfRequired(ex.Message); // re-throw if Required is true
}
finally
{
StateHasChanged(); // always re-render after load
}
}

/// <inheritdoc/>
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (Type == null)
{
BuildFallbackComponent(builder);
return;
}

builder.OpenComponent(0, Type);
builder.AddMultipleAttributes(0, Parameters);
builder.AddComponentReferenceCapture(1, (componentRef) =>
{
Instance = (IComponent)componentRef;
OnAfterLoad?.Invoke(this);
});
builder.CloseComponent();
}

private async Task<(string, string)?> ResolveModuleAndType()
{
var allManifests = await _manifestRepository.GetAllAsync().ConfigureAwait(false);

var manifests = allManifests
Expand Down Expand Up @@ -98,53 +212,19 @@ protected override async Task OnInitializedAsync()

if (bestMatches == null || !bestMatches.Any())
{
DisplayErrorView();
ThrowIfRequired($"Unable to find lazy component '{Name}'. Required: {(Required ? "true" : "false")}");
return;
return null;
}

if (bestMatches.Count > 1)
{
DisplayErrorView();
ThrowIfRequired($"Multiple matches for Component with name '{Name}': '{string.Join(";", bestMatches.Select(m => m.Match.TypeFullName))}'");
return;
return null;
}

var bestMatch = bestMatches.First();

Assembly? componentAssembly = await _assemblyLoader
.LoadAssemblyByNameAsync(new AssemblyName
{
Name = bestMatch.Manifest.ModuleName,
Version = null,
})
.ConfigureAwait(false);

Type = componentAssembly?.GetType(bestMatch.Match.TypeFullName);

if (Type == null)
{
DisplayErrorView(false);
}

StateHasChanged();
}

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (Type == null)
{
BuildFallbackComponent(builder);
return;
}

builder.OpenComponent(0, Type);
builder.AddComponentReferenceCapture(1, (componentRef) =>
{
Instance = (IComponent)componentRef;
OnAfterLoad?.Invoke(this);
});
builder.CloseComponent();
return (bestMatch.Manifest.ModuleName, bestMatch.Match.TypeFullName);
}

private void BuildFallbackComponent(RenderTreeBuilder builder)
Expand Down

0 comments on commit 3be060c

Please sign in to comment.