如何在ASP.NET Razor页面中动态设置Model类型?
问题描述
我有如下数据库上下文类:
public partial class MainDB_Context : DbContext { public MainDB_Context() { } public MainDB_Context(DbContextOptions<MainDB_Context> options) : base(options) { } public virtual DbSet<Chosen> Chosens { get; set; } public virtual DbSet<BtnMsg> BtnMsgs { get; set; } public virtual DbSet<GameRoster> GameRosters { get; set; } ... }
还有泛型页面模型:
public class Admin_IndexModel<T> : PageModel where T : class { readonly MainDB_Context dbContext; readonly List<string> DbSetNames; readonly DbSet<T> DbSet; readonly List<PropertyInfo> DbSetPropInfo; readonly List<string> DbSetPKeys; public List<T> AsyncDbSetItems; ... }
我想通过单个Razor页面展示数据库中的表,对应URL如下:
/Admin/Chosen /Admin/BtnMsg /Admin/GameRoster
希望每个URL对应的@model类型分别是:
Admin_IndexModel<Chosen> Admin_IndexModel<BtnMsg> Admin_IndexModel<GameRoster>
当前我的Razor页面代码如下:
@page "{DbSetName?}" @using System.Reflection; @using asp_net_sql.Models @using asp_net_sql.Pages @model Admin_IndexModel<Chosen> @{ var DbSetNames = ViewData["DbSetNames"] as List<string>; var DbSetPropInfo = ViewData["DbSetPropInfo"] as List<PropertyInfo>; ... } <menu id="adminMenu"> @foreach (var menuItem in DbSetNames) { <div class="menu">@menuItem</div> } </menu> <form method="post"> <table class="table"> <thead> <tr> @foreach (var pInf in DbSetPropInfo) { <th>@pInf.Name</th> } <th> </th> </tr> </thead> <tbody> @{ foreach (var item in Model.AsyncDbSetItems) { row++; var btnId = $"btn_{row}"; <tr> @foreach (var pInf in DbSetPropInfo) { var inpId = $"inp_{pInf.Name}_{row}"; <td> <input id="@inpId" type="text" value="@pInf.GetValue(item)" /> </td> } <td> <button id="@btnId" type="submit" asp-page-handler="Update"> Change (Post) </button> </td> </tr> } } </tbody> </table> </form>
请问如何通过DbSetName动态生成@model的表达式?我的项目没有Views或Controllers。
解决方案
Razor页面的@model是编译时确定的,无法直接动态指定泛型类型,但可以通过以下两种方式实现需求:
方式一:非泛型基类+反射适配
1. 抽离非泛型基类页面模型
把通用逻辑和属性放到非泛型基类中,让泛型模型继承它:
public abstract class Admin_BaseIndexModel : PageModel { protected readonly MainDB_Context _dbContext; public List<string> DbSetNames { get; protected set; } public List<PropertyInfo> DbSetPropInfo { get; protected set; } public List<object> DbSetItems { get; protected set; } public List<string> DbSetPKeys { get; protected set; } protected Admin_BaseIndexModel(MainDB_Context context) { _dbContext = context; // 初始化所有DbSet名称列表 DbSetNames = _dbContext.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) .Select(p => p.PropertyType.GetGenericArguments()[0].Name) .ToList(); } // 抽象方法,由子类实现数据加载逻辑 public abstract Task OnGetAsync(string dbSetName); }
2. 修改泛型页面模型
让泛型模型继承非泛型基类,实现数据加载:
public class Admin_IndexModel<T> : Admin_BaseIndexModel where T : class { public new List<T> DbSetItems => base.DbSetItems.Cast<T>().ToList(); public Admin_IndexModel(MainDB_Context context) : base(context) { } public override async Task OnGetAsync(string dbSetName) { var dbSet = _dbContext.Set<T>(); DbSetItems = await dbSet.ToListAsync(); // 获取当前实体的属性信息 DbSetPropInfo = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList(); // 获取主键字段 DbSetPKeys = typeof(T).GetProperties() .Where(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Any()) .Select(p => p.Name) .ToList(); } }
3. 修改Razor页面
将@model指定为非泛型基类,通过反射动态实例化对应泛型模型:
@page "{DbSetName?}" @model Admin_BaseIndexModel @using System.Reflection; @using asp_net_sql.Models @using asp_net_sql.Pages @{ var targetDbSetName = RouteData.Values["DbSetName"]?.ToString(); List<PropertyInfo> currentPropInfo = null; List<object> currentItems = null; if (!string.IsNullOrEmpty(targetDbSetName)) { // 反射获取实体类型 var entityType = typeof(MainDB_Context).Assembly.GetType($"asp_net_sql.Models.{targetDbSetName}"); if (entityType != null) { // 动态创建泛型模型实例 var genericModelType = typeof(Admin_IndexModel<>).MakeGenericType(entityType); var modelInstance = Activator.CreateInstance(genericModelType, Context.RequestServices.GetService(typeof(MainDB_Context))); // 调用OnGetAsync加载数据 var onGetMethod = genericModelType.GetMethod("OnGetAsync"); await onGetMethod.Invoke(modelInstance, new object[] { targetDbSetName }); // 从基类获取数据 var baseModel = (Admin_BaseIndexModel)modelInstance; currentPropInfo = baseModel.DbSetPropInfo; currentItems = baseModel.DbSetItems; } } } <menu id="adminMenu"> @foreach (var menuItem in Model.DbSetNames) { <a href="/Admin/@menuItem" class="menu">@menuItem</a> } </menu> @if (currentPropInfo != null && currentItems != null) { <form method="post"> <table class="table"> <thead> <tr> @foreach (var pInf in currentPropInfo) { <th>@pInf.Name</th> } <th> </th> </tr> </thead> <tbody> @{ int row = 0; foreach (var item in currentItems) { row++; var btnId = $"btn_{row}"; <tr> @foreach (var pInf in currentPropInfo) { var inpId = $"inp_{pInf.Name}_{row}"; <td> <input id="@inpId" type="text" value="@pInf.GetValue(item)" /> </td> } <td> <button id="@btnId" type="submit" asp-page-handler="Update" asp-route-DbSetName="@targetDbSetName"> Change (Post) </button> </td> </tr> } } </tbody> </table> </form> }
4. 处理Post更新请求
在非泛型基类中添加通用更新方法,通过反射处理不同实体的更新:
public async Task<IActionResult> OnPostUpdateAsync(string DbSetName) { var entityType = typeof(MainDB_Context).Assembly.GetType($"asp_net_sql.Models.{DbSetName}"); if (entityType == null) return NotFound(); var entity = Activator.CreateInstance(entityType); // 绑定表单数据到实体 await TryUpdateModelAsync(entity); if (!ModelState.IsValid) return Page(); _dbContext.Update(entity); await _dbContext.SaveChangesAsync(); return RedirectToPage(new { DbSetName }); }
方式二:自定义路由模型提供者(更简洁)
1. 创建路由模型提供者
根据DbSet名称动态生成对应泛型页面的路由:
public class AdminPageRouteProvider : IPageRouteModelProvider { public int Order => -100; public void OnProvidersExecuting(PageRouteModelProviderContext context) { // 找到Admin目录下的Index页面 var adminIndexPage = context.RouteModels.FirstOrDefault(p => p.RelativePath == "/Pages/Admin/Index.cshtml"); if (adminIndexPage == null) return; // 获取所有DbSet对应的实体类型 var dbContextType = typeof(MainDB_Context); var entityTypes = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) .Select(p => p.PropertyType.GetGenericArguments()[0]) .ToList(); // 为每个实体生成专属路由 foreach (var entityType in entityTypes) { var routeModel = new PageRouteModel(adminIndexPage) { RouteTemplate = $"Admin/{entityType.Name}", PageType = typeof(Admin_IndexModel<>).MakeGenericType(entityType) }; context.RouteModels.Add(routeModel); } } public void OnProvidersExecuted(PageRouteModelProviderContext context) { // 无需额外处理 } }
2. 注册路由提供者
在Program.cs中添加注册代码:
builder.Services.AddSingleton<IPageRouteModelProvider, AdminPageRouteProvider>();
3. 修改Razor页面
将@model改为dynamic,运行时动态解析泛型模型的属性:
@page "{DbSetName?}" @using System.Reflection; @using asp_net_sql.Models @using asp_net_sql.Pages @model dynamic @{ var DbSetNames = Model.DbSetNames; var DbSetPropInfo = Model.DbSetPropInfo; var AsyncDbSetItems = Model.AsyncDbSetItems; } <menu id="adminMenu"> @foreach (var menuItem in DbSetNames) { <a href="/Admin/@menuItem" class="menu">@menuItem</a> } </menu> <form method="post"> <table class="table"> <thead> <tr> @foreach (var pInf in DbSetPropInfo) { <th>@pInf.Name</th> } <th> </th> </tr> </thead> <tbody> @{ int row = 0; foreach (var item in AsyncDbSetItems) { row++; var btnId = $"btn_{row}"; <tr> @foreach (var pInf in DbSetPropInfo) { var inpId = $"inp_{pInf.Name}_{row}"; <td> <input id="@inpId" type="text" value="@pInf.GetValue(item)" /> </td> } <td> <button id="@btnId" type="submit" asp-page-handler="Update" asp-route-DbSetName="@RouteData.Values["DbSetName"]"> Change (Post) </button> </td> </tr> } } </tbody> </table> </form>
注意:使用dynamic会失去编译时类型检查,需确保模型属性名称完全匹配。
内容的提问来源于stack exchange,提问作者user9163823
相关产品推荐
相关产品推荐

