简体   繁体   English

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

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

I have created a few Razor Components that are in a RCL and I would like to load and display them at runtime.我在 RCL 中创建了一些 Razor 组件,我想在运行时加载和显示它们。 I am aware that I can load assemblies using reflection, however I want the page to automatically update the menu and display the proper component.我知道我可以使用反射加载程序集,但是我希望页面自动更新菜单并显示正确的组件。 This should be able to be done by simply dropping the DLL in the specified directory.这应该可以通过简单地将 DLL 放到指定目录中来完成。

So far I have determined that each page should have it's own class that implements an interface to ensure each page has the necessary information.到目前为止,我已经确定每个页面都应该有自己的类来实现一个接口,以确保每个页面都有必要的信息。 The interface that I have come up with is我想出的界面是

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

And I am able to load this into memory by using the following:我可以使用以下方法将其加载到内存中:

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);
    }
}

But how do I go about updating the UI (the page and the navigation menu) to reflect these components?但是如何更新 UI(页面和导航菜单)以反映这些组件?

This is something I was trying to accomplish the other day and figured I'd post here to document for others if anyone else is running into a similar issue.这是我前几天试图完成的事情,并认为如果其他人遇到类似问题,我会在这里发布以供其他人记录。 The first step I took to resolving this was to create a new .net standard 2.0 project and add in these items:我解决这个问题的第一步是创建一个新的 .net 标准 2.0 项目并添加以下项目:

An IComponentService interface to allow for easy DI injection, and possibly different implementations should the need arise一个 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);
}

The implementation of the IComponentService, this is primarily used for loading the components/pages and keeping track of them. 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);
        }
    }
}

The IDynamicComponent interface for which there should be one implementation for each page that you want loaded IDynamicComponent 接口,您要加载的每个页面都应该有一个实现

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

And a simple MenuItem class that will contain information for the navigation menu还有一个简单的 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; }
}

Setting up the Component设置组件

Next step was to set up the page.下一步是设置页面。 I started by taking the built-in demo WeatherForecast and moving all associated files to a separate project as a RCL .我首先使用内置演示 WeatherForecast 并将所有相关文件作为RCL移动到一个单独的项目中。 Following this I modified the .razor file to not inject the WeatherForecastService but rather instantiate a new copy of it as shown below:在此之后,我修改了 .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);
    }
}

Next I created a class called MyComponent and added it to the project containing the WeatherForecast接下来,我创建了一个名为 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"
    };
}

It is important to note that the Parameters dictionary contains an entry called "Name" which is the name of the Parameter for the WeatherForecast page.请务必注意,参数字典包含一个名为“名称”的条目,它是 WeatherForecast 页面的参数名称。 This allows us to change and inject different parameters at runtime.这允许我们在运行时更改和注入不同的参数。 The "Page" property is to create the url for the page (ex. /Forecast /Counter etc) “页面”属性是为页面创建 url(例如 /Forecast /Counter 等)

Fixing the base project修复基础项目

Once the component was setup and the other project containing the componentservice I had to modify the base Blazor project to take advantage of these changes.设置组件和包含 componentservice 的另一个项目后,我必须修改基础 Blazor 项目以利用这些更改。

First I started by adding the IComponentService to the DI container by adding the following code to the ConfigureServices method in the Startup.cs file首先,我通过将以下代码添加到 Startup.cs 文件中的 ConfigureServices 方法,将 IComponentService 添加到 DI 容器

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

Next I created a simple extension method that would convert a MenuItem into a component using the RenderFragment builder接下来我创建了一个简单的扩展方法,可以使用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;
    }

The next phase was to modify the navigation menu to load all components by generating the renderfragments from the MenuItems and showing them.下一阶段是通过从 MenuItems 生成渲染片段并显示它们来修改导航菜单以加载所有组件。 In NavMenu.razor I edited the file to match this:在 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();
    }
}

And for the final step I created a new page in the Pages directory called ComponentPage to display the new page.对于最后一步,我在 Pages 目录中创建了一个名为 ComponentPage 的新页面来显示新页面。 This is done by utilizing the RenderFragment builder.这是通过使用 RenderFragment 构建器来完成的。 We open the page and add any parameters then display the results on the page.我们打开页面并添加任何参数,然后在页面上显示结果。

@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();
    };
}

The result was being able to load full pages and modify the navmenu at runtime.结果是能够在运行时加载完整页面并修改导航菜单。

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

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