这是我前几天试图完成的事情,并认为如果其他人遇到类似问题,我会在此处发布以记录其他人。我解决此问题的第一步是创建一个新的 .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();
};
}
结果是能够加载整个页面并在运行时修改导航菜单。