How does Asp.NET MVC translate ActionResult into Html? (Part 1)

Introduction

As we all know, the UI of the website is rendered by web browsers. And the web browsers create kinds of controls depends on the HTML code provided by the website. So, when we visit a website, the web browsers will try to get the HTML code of the website and the relative sources it needed according to Http protocol, then the browsers will create and render all the controls in this website correctly.

The View in the ASP.NET MVC determines the rendering of the website. And we can notice that the name of View is the same as Action‘s name. When an Action is executed, the ASP.NET MVC will get the View as a result by the name of the Action. Then the View will be translated into HTML code and sent to browsers as a return.

So, the rendering in the ASP.NET is about how to create the correct HTML code.

Here is the code of a simple Action.

1
2
3
4
public ActionResult Index()
{
return View();
}

In this method, it returns a ViewResult as a result. And how does it work? How does the ASP.NET find the correct view? And how to translate it into HTML code?

I will try to figure out the first question in this article (part one). And we will discuss the second question in my next blog (part two).

Details

All the source code about the ASP.NET is provided by Microsoft. And the code is posted on the Github, for more detail, visit AspNetWebStack.

Controller

The Controller class inherits the ControllerBase class. And here is the source code of the Execute method in the ControllerBase class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected virtual void Execute(RequestContext requestContext)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
if (requestContext.HttpContext == null)
{
throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext");
}

VerifyExecuteCalledOnce();
Initialize(requestContext);

using (ScopeStorage.CreateTransientScope())
{
ExecuteCore();
}
}

protected abstract void ExecuteCore();

And as the code shows, when the Execute method is executed, it will call the ExecuteCore method which is an abstract method and this method is implemented in the Controller class which is the only subclass of ControllerBase.

Here is the source code of ExecuteCore in Controller class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected override void ExecuteCore()
{
// If code in this method needs to be updated, please also check the BeginExecuteCore() and
// EndExecuteCore() methods of AsyncController to see if that code also must be updated.
PossiblyLoadTempData();
try
{
string actionName = GetActionName(RouteData);
if (!ActionInvoker.InvokeAction(ControllerContext, actionName))
{
HandleUnknownAction(actionName);
}
}
finally
{
PossiblySaveTempData();
}
}

It is easy to notice that there are four steps in the ExecuteCore method.

  1. Load temp data
  2. Get the name of action by the RouteData
  3. Call the ActionInvoker.InvokeAction method to execute the action which’s name is equal to the actionName.
  4. Save temp data

And what does ActionInvoker.InvokeAction do? How does it translate the ActionResult into the HTML code? Reading the source code with these questions, I found that in the InvokeAction method it calls ActionResult.ExecuteResult after getting the result from the action method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
{
//some code is omitted here
...
if (authenticationContext.Result != null)
{
//some code is omitted here
...
InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result);
}
else
{
// some code is omitted here
...
if (authorizationContext.Result != null)
{
//some code is omitted here
...
InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result);
}
else
{
//some code is omitted here
...
//In this method , it calls InvokeActionResult too.
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters,
challengeContext.Result ?? postActionContext.Result);
}
}
}

protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
{
actionResult.ExecuteResult(controllerContext);
}

So, I guess that the secret of how does ASP.NET translate the ActionResult into Html code is in the implementation of ActionResult class and its subclasses.

ViewResult

What does the ActionResult look like? What does the ExecuteResult method do? Let’s see the code of ActionResult class.

1
2
3
4
public abstract class ActionResult
{
public abstract void ExecuteResult(ControllerContext context);
}

The ActionResult is an abstract class and the ExecuteResult method is an abstract method too. Obviously, the correct implementation is in the subclass. So, I read the ExecuteResult method in the ViewResultBase class which is the base class of ViewResult and inherits the ActionResult class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}

ViewEngineResult result = null;

if (View == null)
{
result = FindView(context);
View = result.View;
}

TextWriter writer = context.HttpContext.Response.Output;
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
View.Render(viewContext, writer);

if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
}

After reading this method, I am sure that this method is the core of translating the view into HTML. And there are 4 steps.

  1. If the ViewName is empty, it will set the name of Action as view’s name.
  2. Find the correct View instance depends on the context
  3. Call the Render method of the View instance to write the content of the View into the information of the HttpContext, and then return it to the browser.
  4. Release the view‘s data

So, we can split the translation into two parts. The first part is to find the correct View. And the second part is to translate.

How to find the correct view

The View files are named with the name of the action method, and these files are always placed in the directory which is named with the name of the Controller class. And this directory will be placed in the View directory in the root directory.

How does the ViewResult find the correct View?

Here is the source code of FindView method in the ViewResult class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected override ViewEngineResult FindView(ControllerContext context)
{
ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
if (result.View != null)
{
return result;
}

// we need to generate an exception containing all the locations we searched
StringBuilder locationsText = new StringBuilder();
foreach (string location in result.SearchedLocations)
{
locationsText.AppendLine();
locationsText.Append(location);
}
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.Common_ViewNotFound, ViewName, locationsText));
}

We can see from the code that the FindView method calls ViewEngineCollection.FindView method to find the View with the ViewName and MaterName. If the View is found, this method will return a ViewEngineResult instance, if not the ViewEngineResult.FindView will throw an InvalidOperationException exception.

And there are two classes we have not seen before. One is the ViewEngineCollection class and the other is the ViewEngineResult class. So, let’s continue reading the code of these classes to figure out what do they do.

ViewEngineCollection

After reading the code about ViewEngineCollection, I found that all of the instances of ViewEngineCollection are managed by a static class named ViewEngines. And the code in ViewEngines is quite simple.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class ViewEngines
{
private static readonly ViewEngineCollection _engines = new ViewEngineCollection
{
new WebFormViewEngine(),
new RazorViewEngine(),
};

public static ViewEngineCollection Engines
{
get { return _engines; }
}
}

It is easy to find that there are two special classes in the code, WebFormViewEngine and RazorViewEngine. In the MVC application, we use the RazorViewEngine class to render the View.

Here is the inheritance map of the WebFormViewEngine and the RazorViewEngine.

We can get some information from this picture.

  1. Their base class is BuildManagerViewEngine class, it means that these two classes are associated with building or compiling.
  2. The base class of BuildManagerViewEngine is the VirtualPathProviderViewEngine, it tells us that these two classes are based on the relative path to manage View.

In this article, we are talking about ASP.NET MVC. So, we ignore the WebFormViewEngine and continue reading the code of RazorViewEngine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RazorViewEngine : BuildManagerViewEngine
{
internal static readonly string ViewStartFileName = "_ViewStart";

//Some code is omitted here
...

public RazorViewEngine(IViewPageActivator viewPageActivator)
: base(viewPageActivator)
{
AreaViewLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
AreaMasterLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
//Some code is omitted here
...
}
//Some code is omitted here
...
}

It is important to notice that there is a magic string named ViewStartFileName, and its value is _ViewStart. So, it means the _ViewStart is the start page without changing. And we can see that the RazorViewEngine class set lots of location format strings when it is initializing, and these formats specify the search path for the pages.

Finally, I found the implementation of the FindView method in the VirtualPathProviderViewEngine class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public virtual ViewEngineResult FindView(ControllerContextcontrollerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (String.IsNullOrEmpty(viewName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
}
string[] viewLocationsSearched;
string[] masterLocationsSearched;
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched);
string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched);
if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
}
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}

And there are 4 steps to create an instance of the ViewEngineResult.

  1. Find the correct name of the controller by the RouteData
  2. Get the file path of the View by passing the viewName, masterName and controllerName into the GetPath method
  3. Get the master’s file path by calling GetPath method
  4. New an instance of ViewEngineResult and return it

And the GetPath method needs the LocationFormats we mentioned above to find the correct path.

Conclusion

Overall, we probably know that how does ASP.NET MVC finds the correct View file. When a Controller instance is executed, it will call ActionInvoker.InvokeAction method to execute the correct Action by the actionName. And in the ActionInvoker.InvokeAction method, it calls ActionResult.ExecuteResult method, in this method the ViewResult.FindView method is called to find the correct View. And after reading the code about RazorViewEngine and ViewEngineCollection we can know that the RazorViewEngine set lots of location formats to help the VirtualPathProviderViewnEngine.FindView method to find the correct View by relative path.