繁体   English   中英

如何在运行时加载 Blazor 页面?

[英]How can I load a Blazor page at runtime?

我在 RCL 中创建了一些 Razor 组件,我想在运行时加载和显示它们。 我知道我可以使用反射加载程序集,但是我希望页面自动更新菜单并显示正确的组件。 这应该可以通过简单地将 DLL 放到指定目录中来完成。

到目前为止,我已经确定每个页面都应该有自己的类来实现一个接口,以确保每个页面都有必要的信息。 我想出的界面是

public interface IDynamicComponent
{
    IDictionary<string,string> Parameters { get; }
    string Name { get; }
    string Page { get; }
    Type Component { get;}
    MenuItem MenuData { get; }
}

我可以使用以下方法将其加载到内存中:

public IEnumerable<Type> LoadComponents(string path)
{
    var components = new List<Type>();
    var assemblies = LoadAssemblies(path);

    foreach (var asm in assemblies)
    {
        var types = GetTypesWithInterface(asm);
        foreach (var typ in types) components.Add(typ);
    }

    Components = components;
}

private IEnumerable<Type> GetTypesWithInterface(Assembly asm)
{
    var it = typeof(IDynamicComponent);
    return GetLoadableTypes(asm).Where(it.IsAssignableFrom).ToList();
}

private IEnumerable<Type> GetLoadableTypes(Assembly assembly)
{
    if (assembly == null) throw new ArgumentNullException("assembly");
    try
    {
        return assembly.GetTypes();
    }
    catch (ReflectionTypeLoadException e)
    {
        return e.Types.Where(t => t != null);
    }
}

但是如何更新 UI(页面和导航菜单)以反映这些组件?

这是我前几天试图完成的事情,并认为如果其他人遇到类似问题,我会在这里发布以供其他人记录。 我解决这个问题的第一步是创建一个新的 .net 标准 2.0 项目并添加以下项目:

一个 IComponentService 接口,允许简单的 DI 注入,并且在需要时可能有不同的实现

public interface IComponentService
{
    void LoadComponents(string path);
    IDynamicComponent GetComponentByName(string name);
    IDynamicComponent GetComponentByPage(string name);
    IEnumerable<Type> Components { get; }
    IEnumerable<MenuItem> GetMenuItems(bool getHiddenItems = false);
}

IComponentService 的实现,主要用于加载组件/页面并跟踪它们。

public class ComponentService : IComponentService
{
    public IEnumerable<Type> Components { get; private set; }

    public void LoadComponents(string path)
    {
        var components = new List<Type>();
        var assemblies = LoadAssemblies(path);

        foreach (var asm in assemblies)
        {
            var types = GetTypesWithInterface(asm);
            foreach (var typ in types) components.Add(typ);
        }

        Components = components;
    }

    public IEnumerable<MenuItem> GetMenuItems(bool getHiddenItems = false)
    {
        var components = Components.Select(x => (IDynamicComponent) Activator.CreateInstance(x));
        if (!getHiddenItems)
            components = components.Where(x => x.MenuData.Display);
        
        return components.Select(x=>x.MenuData);
    }

    public IDynamicComponent GetComponentByName(string name)
    {
        return Components.Select(x => (IDynamicComponent) Activator.CreateInstance(x))
            .SingleOrDefault(x => x.Name == name);
    }
    
    public IDynamicComponent GetComponentByPage(string name)
    {
        return Components.Select(x => (IDynamicComponent) Activator.CreateInstance(x))
            .SingleOrDefault(x => x.Page == name);
    }

    private IEnumerable<Assembly> LoadAssemblies(string path)
    {
        return Directory.GetFiles(path, "*.dll").Select(dll => Assembly.LoadFile(dll)).ToList();
    }

    private IEnumerable<Type> GetTypesWithInterface(Assembly asm)
    {
        var it = typeof(IDynamicComponent);
        return GetLoadableTypes(asm).Where(it.IsAssignableFrom).ToList();
    }

    private IEnumerable<Type> GetLoadableTypes(Assembly assembly)
    {
        if (assembly == null) throw new ArgumentNullException("assembly");
        try
        {
            return assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException e)
        {
            return e.Types.Where(t => t != null);
        }
    }
}

IDynamicComponent 接口,您要加载的每个页面都应该有一个实现

public interface IDynamicComponent
{
    IDictionary<string,string> Parameters { get; }
    string Name { get; }
    string Page { get; }
    Type Component { get;}
    MenuItem MenuData { get; }
}

还有一个简单的 MenuItem 类,它将包含导航菜单的信息

public class MenuItem
{
    public bool Display { get; set; }
    public string Text { get; set; }
    public string Page { get; set; }
    public string Icon { get; set; }
    public string CSS { get; set; }
}

设置组件

下一步是设置页面。 我首先使用内置演示 WeatherForecast 并将所有相关文件作为RCL移动到一个单独的项目中。 在此之后,我修改了 .razor 文件以不注入 WeatherForecastService 而是实例化它的新副本,如下所示:

@code {
    [Parameter]
    public string Name { get; set; }
    
    private WeatherForecast[] forecasts;
    private WeatherForecastService WeatherForecastService;

    protected override async Task OnInitializedAsync()
    {
        WeatherForecastService = new WeatherForecastService();
        forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
    }
}

接下来,我创建了一个名为 MyComponent 的类并将其添加到包含 WeatherForecast 的项目中

public class MyComponent : IDynamicComponent
{
    public bool DisplayInMenu => true;
    
    public IDictionary<string,string> Parameters => new Dictionary<string,string>
    {
        {"Name","My Weather Forecast"}
    };
    
    public string Name => "Weather Forecast";
    public string Page => "Forecast";
    public Type Component => typeof(Component2);

    public MenuItem MenuData => new MenuItem
    {
        Display = true,
        Page = Page,
        CSS = String.Empty,
        Text = "Data",
        Icon = "oi oi-list-rich"
    };
}

请务必注意,参数字典包含一个名为“名称”的条目,它是 WeatherForecast 页面的参数名称。 这允许我们在运行时更改和注入不同的参数。 “页面”属性是为页面创建 url(例如 /Forecast /Counter 等)

修复基础项目

设置组件和包含 componentservice 的另一个项目后,我必须修改基础 Blazor 项目以利用这些更改。

首先,我通过将以下代码添加到 Startup.cs 文件中的 ConfigureServices 方法,将 IComponentService 添加到 DI 容器

        services.AddSingleton<IComponentService>(_ =>
        {
            var service = new ComponentService();
            service.LoadComponents(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
            return service;
        });

接下来我创建了一个简单的扩展方法,可以使用RenderFragment 构建器将 MenuItem 转换为组件

    public static RenderFragment GenerateMenuItem(this MenuItem item)
    {
        RenderFragment fragment = builder =>
        {
            builder.OpenElement(3, "li");
            builder.AddAttribute(4,"class","nav-item px-3");
            builder.OpenComponent<NavLink>(4);
            builder.AddAttribute(6,"class","nav-link");
            builder.AddAttribute(7, "href", $"/{item.Page}");
            builder.AddAttribute(8, "Match", NavLinkMatch.All);
            builder.AddAttribute(9, "ChildContent", (RenderFragment)((builder2) => {
                builder2.AddMarkupContent(10, $"<span class=\"{item.Icon}\" aria-hidden=\"true\"></span>");
                builder2.AddContent(11, item.Text);
            }));
            builder.CloseComponent();
            builder.CloseElement();
        };
        return fragment;
    }

下一阶段是通过从 MenuItems 生成渲染片段并显示它们来修改导航菜单以加载所有组件。 在 NavMenu.razor 中,我编辑了文件以匹配以下内容:

@using Component.Common
@inject IComponentService ComponentService

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">BlazorComponentHotloadDemo</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        @if (menuItems != null)
        {
            foreach (var fragment in menuItems)
                @fragment;
        }
    </ul>
</div>

@code {
    IEnumerable<RenderFragment> menuItems;
    private bool collapseNavMenu = true;
    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }

    protected override void OnInitialized()
    {
        var items = ComponentService.GetMenuItems();

        var menulist = new List<RenderFragment>();
        foreach (var item in items)
        {
            menulist.Add(item.GenerateMenuItem());
        }
        menuItems = menulist;
        base.OnInitialized();
    }
}

对于最后一步,我在 Pages 目录中创建了一个名为 ComponentPage 的新页面来显示新页面。 这是通过使用 RenderFragment 构建器来完成的。 我们打开页面并添加任何参数,然后在页面上显示结果。

@page "/{componentName}"
@using Component.Common
@inject IComponentService ComponentService

@dynamicComonent()

@code{
    [Parameter]
    public string componentName { get; set; }
   
    RenderFragment dynamicComonent() => builder =>
    {
        var component = ComponentService.GetComponentByPage(componentName);
        builder.OpenComponent(0,component.Component);
        
        for (int i = 0; i < component.Parameters.Count; i++)
        {
            var attribute = component.Parameters.ElementAt(i);
            builder.AddAttribute(i+1,attribute.Key,attribute.Value);
        }
        
        builder.CloseComponent();
    };
}

结果是能够在运行时加载完整页面并修改导航菜单。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM