整个过程非常复杂,由ResourceInvoker class 内部管理,但负责处理ViewResult 的主要管道是ViewExecutor class (source code)。
ViewExecutor 的主要入口点是ExecuteAsync() method,它接受ActionContext 和您的ViewResult。然后它使用注册的IViewEngine(通常是RazorViewEngine)定位适当的视图以识别相应的视图并返回ViewEngineResult instance。然后使用 ViewEngineResult 执行和渲染视图。
扩展ViewExecutor
如果您需要自定义逻辑,您可以通过实现IActionResultExecutor<> interface 来实现自己的视图执行器类,然后使用 ASP.NET Core 的依赖注入容器将其注册为单例。这样做时,我建议参考the out-of-the-box ViewResultExecutor's source code。
如果您想实现自定义逻辑来定位视图,这会很有用。例如,您可能想要考虑状态或上下文数据,例如路由数据、请求标头、cookie、会话等。定位视图时。
因此,作为一个非常简单的示例,假设您希望允许使用查询字符串值(例如,?View=MyView)选择性地选择视图。在这种情况下,您可以创建一个QueryStringViewResultExecutor。这是一个基本的概念验证:
public class QueryStringViewResultExecutor : ViewExecutor, IActionResultExecutor<ViewResult>
{
public QueryStringViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
ITempDataDictionaryFactory tempDataFactory,
DiagnosticListener diagnosticListener,
IModelMetadataProvider modelMetadataProvider
) : base(
viewOptions, writerFactory, viewEngine, tempDataFactory, diagnosticListener, modelMetadataProvider
)
{
}
public async Task ExecuteAsync(ActionContext context, ViewResult result)
{
var viewEngineResult = FindView(context, result); // See helper method below
viewEngineResult.EnsureSuccessful(originalLocations: null);
var view = viewEngineResult.View;
using (view as IDisposable)
{
await ExecuteAsync(
context,
view,
result.ViewData,
result.TempData,
result.ViewName,
result.StatusCode
).ConfigureAwait(false);
}
}
private ViewEngineResult FindView(ActionContext actionContext, ViewResult viewResult)
{
// Define variables
var view = (ViewEngineResult?)null;
var viewEngine = viewResult.ViewEngine?? ViewEngine;
var searchedPaths = new List<string>();
var requestContext = actionContext.HttpContext.Request;
// If defined, attempt to locate view based on query string variable
if (requestContext.Query.ContainsKey("View"))
{
var queryStringValue = requestContext.Query["View"].First<string>();
if (queryStringValue is not null)
{
view = viewEngine.FindView(actionContext, queryStringValue, isMainPage: true);
searchedPaths = searchedPaths.Union(view.SearchedLocations?? Array.Empty<string>()).ToList();
}
}
// If no view is found, fall back to the view defined on the viewResult
if (!view?.Success?? true)
{
view = viewEngine.FindView(actionContext, viewResult.ViewName, isMainPage: true);
searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty<string>()).ToList();
}
// Return view for processing by the razor engine
if (view is not null and { Success: true }) {
return view;
}
return ViewEngineResult.NotFound(viewResult.ViewName, searchedPaths);
}
}
然后您将在您的 Startup.ConfigureServices() 方法中将其注册为:
services.Services.TryAddSingleton<IActionResultExecutor<ViewResult>, QueryStringViewResultExecutor>();
免责声明:您可能需要取消注册开箱即用的ViewExecutor 以避免冲突。但是,通常情况下,您正在使用自定义 ViewResult 注册 IActionResultExecutor<>,因此,这不是必需的;见下文。
扩展ViewResult
通常,您会希望将此与自定义 ViewResult 配对。为什么这很有用?通常是因为您需要从您的Controller 向您的ViewResultExecutor 传递额外的数据。
因此,作为一个人为的示例,假设您有主题,并且可以选择基于该主题自定义视图。然后,您可以将Theme 属性添加到您的ViewResult,从而允许ViewResultExecutor 首先查找基于主题的视图,否则回退到非主题版本。
public class ThemedViewResult : ViewResult
{
public string Theme { get; set; }
public override async Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ThemedViewResult>>();
await executor.ExecuteAsync(context, this).ConfigureAwait(false);
}
}
即使您不需要自定义 ViewResult,也值得暂停一下以评估此代码。底层ViewResult 类实现IActionResult,它公开了一个方法ExecuteResultAsync()。这是由顶部提到的ResourceInvoker 类调用的。这反过来又找到了注册的IActionResultExecutor<>——即我们在上一节中创建和注册的组件类型——并调用它的ExecuteAsync()方法。
注意:这是一个人为的示例,因为其他上下文数据通常可以访问主题,例如RouteData、自定义ClaimsPrincipal 或ISession 实现。但是您可以想象其他时候,这些信息将是特定于请求的,最好通过ViewResult 从Controller 中继。例如,也许是一个 CMS,其中可以按页面选择主题。
由于不清楚您是否真的需要扩展此功能,或者只是好奇所有内容如何组合在一起,所以上述示例仅作为概念验证;他们没有经过测试。不过,它们应该让您大致了解 ViewResult 的处理位置和方式,以及在您需要时扩展该行为的选项。