【问题标题】:How can I load a Blazor page at runtime?如何在运行时加载 Blazor 页面?
【发布时间】:2020-09-18 05:13:43
【问题描述】:

我在 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(页面和导航菜单)以反映这些组件?

【问题讨论】:

    标签: c# .net-core blazor-server-side


    【解决方案1】:

    这是我前几天试图完成的事情,并认为如果其他人遇到类似问题,我会在此处发布以记录其他人。我解决此问题的第一步是创建一个新的 .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"
        };
    }
    

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

    修复基础项目

    一旦设置了组件并且包含组件服务的另一个项目,我必须修改基础 Blazor 项目以利用这些更改。

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

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

    接下来我创建了一个简单的扩展方法,该方法将使用 RenderFragment builder 将 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;
        }
    

    下一阶段是修改导航菜单以通过从 MenuItem 生成渲染片段并显示它们来加载所有组件。在 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();
        };
    }
    

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

    【讨论】:

    • 我认为您应该将 blazor-server 添加到问题标签中。
    • 我想知道这解决了什么问题。在服务器上,您可以只部署 1 个并使用布尔值打开/关闭组件。如果需要,每个用户。在 WebAssembly 上这是行不通的。
    • 感谢您对标签的建议,我更新了问题。至于它解决了什么问题,只是能够在运行时加载它们而无需事先知道它们可能是什么。您可以通过布尔值更改可见性,但这需要先验知识。
    • 另见 oqtane.org 以获取也适用于 WebAssembly 的代码
    • 您好 DCCoder,您的解决方案非常有用。我已经在我的 Blazor Wasm 托管项目中实现了它,但是我在将组件 IDynamicComponentIEnumerable&lt;Type&gt; 从 Blazor 服务器发送到 Blazor wasm 客户端时遇到了问题。在我通过 SignalR 发送到客户端之前,序列化失败。你知道我该怎么做吗?
    猜你喜欢
    • 2022-09-27
    • 1970-01-01
    • 2011-06-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多