outline
Tag Helpers Rendering
Part 2
In the previous post, I discussed how to setup an exploration environment that allows to step into and over the execution of the ASP.NET Core framework and specifically in the Mvc and the Razor projects code bases in order to understand the Tag Helpers rendering mechanism.
The tag helpers used in cshtml views are rendered by the C# classes generated.The ability to follow its execution step by step proved to be useful.
At the end of the last post, I showed the C# class originating from a very simple cshtml file.
Let’s now analyze its execution starting with the generated razor page.
Razor Page Class
Here is the simple cshtml
file that we are considering:
The previous chtml view is compiled to the following C# code:
You will notice that the previous code listing was formatted, actually I removed all the #line
and #pragma
directives and I simplified the fully qualified class names, which makes for code that is less painful to read.
Observations
After scanning the previous code we can make some observations:
First off, I did not know that it was possible to put using
statements inside a namespace
scope to include name spaces. Every day is a school day…
Then, The generated _Views_Home_Index_cshtml
class inherits from the RazorPage
abstract class parametrized with the dynamic
type. Here the dynamic
type is used because the view is not strongly typed.
_Views_Home_Index_cshtml
overrides the ExecuteAsync
method in which the template rendering logic is located.
Finally, some properties in the _Views_Home_Index_cshtml
are set up to contain instances of Tag Helper related classes: TagHelperExecutionContext
, TagHelperRunner
, TagHelperScopeManager
and ImageTagHelper
.
Diving in ExecuteAsync
We are going to focus on the tag helpers related objects, specifically on the following snippet(further tweaked for readability) extracted from the ExecuteAsync
method:
The TagHelperScopeManager Creates a TagHelperExecutionContext
The TagHelperScopeManager.Begin
method returns a TagHelperExecutionContext
after taking the following arguments:
- The name of the to be generated tag
- The mode of the to be generated tag (Open-close, self-closing…)
- A UniqueId string
- A function object that executes any nested tag helpers
Looking inside the TagHelperScopeManager
class shows that it encapsulates a TagHelperExecutionContext
instances pooling behavior which might be there to allow reuse of existing instances.
The responsibility of the scope manager through the Begin
method is to perform checks on the arguments it gets passed, to create a dictionary object needed by the TagHelperExecutionContext
and to delegate the instantiation of the TagHelperExecutionContext
to the ExecutionContextPool
private class.
By default, the scope manager creates a new items Dictionary object. But if it detects that a parentExecutionContext
exists for the current scope it creates a CopyOnWrite
dictionary that is initialized with the parent’s item dictionary (parentExecutionContext.Items
).
The arguments listed earlier along with the created items dictionary are passed to the ExecutionContextPool
object that either returns a freshly created TagHelperExecutionContext
or a reinitialized existing one.
The TagHelperScopeManager.End
seems to be used in nested tag helpers scenarii, although not sure, it looks like it keeps track of levels of nesting.
So to make it short the scope manager creates a TagHelperExecutionContext
.
Tag Helper Execution Context
The TagHelperExecutionContext
object is used to contain the created tag helpers and their attributes.
In our example, the ExecuteAsync
method creates two tag helpers: the expected ImageTagHelper
and a UrlResolutionTagHelper
.
The creation of the UrlResolutionTagHelper
is a bit strange, in our example we just made use of the ImageTagHelper
.
Here we must remember that tag helpers can be activated by attribute and by html element name. It turns out that the UrlResolutionTagHelper
gets activated on the img
html element, among others, in order to resolve relative assets path.
Both the UrlResolutionTagHelper
and the ImageTagHelper
as well as their html attributes are added to the TagHelperExecutionContext
via the Add
, AddHtmlAttribute
and AddTagHelperAttribute
methods.
The responsibility of the tag helper execution context is to hold the state related to multiple tag helpers that gets activated on the same html element.
The constructor of the TagHelperExecutionContext
class accepts the following arguments:
- The name of the tag that is going to be generated
- The mode of the tag that is going to be generated (Open-close, self-closing…)
- The Items dictionary created by the scope manager.
- The UniqueId string
- The function object that executes any nested tag helpers
- The
startTagHelperWritingScope
callback - The
endTagHelperWritingScope
callback
Then it creates a TagHelperOutput
object and a TagHelperContext
object which will be respectively assigned to the Output
and Context
public properties. All the elements needed to fulfill the previous instantiation are passed to TagHelperExecutionContext
’s constructor.
The previous instances will be necessary in order to call the ProcessAsync
method on the tag helpers that needs to be rendered.
The two last arguments of the TagHelperExecutionContext
: startTagHelperWritingScope
and endTagHelperWritingScope
are callbacks defined under the RazorPageBase
class and are used in the TagHelperExecutionContext.SetOutputContentAsync
which executes the nested tag helpers if any. The responsibility of these callbacks is not clear yet, some more on this next time.
Rendering The Tag Helpers
After being populated the with the tag helpers objects and their attributes the TagHelperExecutionContext
is passed to the TagHelperRunner.RunAsync
method which performs the following actions:
- Orders the tag helper objects contained in the context(recall the
TagHelper.Order
property) - Iterates over the ordered tag helpers and calls
TagHelper.Init
on them - Iterates over the ordered tag helpers and calls
ProcessAsync
on each one. TheTagHelperOutput
andTagHelperContext
contained in the execution context are used.
The result of the processing is available in the TagHelperExecutionContext.Output
property and is further written to the current page by the RazorPage.Write
.
Looking inside the Write
method shows that the TagHelperOutput.WriteTo
is used to generate the final html content.
Summary
The complexity introduced by the collaboration between the TagHelperScopeManager
, TagHelperExecutionContext
and TagHelperRunner
is necessary to handle multiple tag helpers activation on the same html element as well as to manage nested tag helpers processing.
Nested tag helpers processing is still not clear and is going to be the subject of an upcoming post some day.