Passing Data into Templates
As we noted earlier, to pass data into a template, we need to create a TemplateContext
. This can be very simple – like passing in a model or using SetValue
, but can become a little more complex if we include other templates, or specifically take advantage of “Scopes.”
In general, a Context holds all the data available to the template during its execution. This is also the “scratchpad” on which the template itself will write data when you assign variables. There is one Context per execution.
Inside a Context are a hierarchy of Scopes. There is always more than one Scope in a Context. We’ll discuss that more below.
But first, a quick review.
There are two ways to get data into a Context which is then accessible to our template.
We can pass an object into the TemplateContext
constructor.
var content = new TemplateContent(new { name = "Deane" });
var result = template.Render(context);
All of the properties on the model are extracted into values the template can use.
My name is {{ name }}.
My name is Deane.
Using a model is optional. Alternately, we can accomplish the same thing by pushing individual values into the Context with SetValue
.
var context = new TemplateContext();
context.SetValue("name", "Deane");
var result = template.Render(context);
There is a subtle difference between the Context and the Model which affects how a template “finds” a value to render.
When you “put something in the Context,” like this –
context.SetValue("name", "Deane");
– you’re actually putting it in a “Scope.” Scopes hold values in a simple dictionary.
There is a hierarchy of Scopes inside the Context – each Scope has a parent. When a template attempts to retrieve a value, it will look in its most immediate Scope, then work its way up through the ancestor Scopes. It will return the first matching value it finds for the key.
The highest possible Scope is on TemplateOptions.Scope
. This is the common ancestor of all Scopes. Something put here can be considered “global” to all templates, unless “hidden” by a child Scope (keep reading…).
Next down the list is the LocalScope
on a specific Context.
Next down the list are the Scopes on specific templates – every template has its own Scope. Every template gets a Scope, and a new child Scope is created for every template added using an include
statement.
This means you will always have a minimum of three Scopes – the global, the Context, and whatever template you are executing.
You get another Scope for every include
. If you have a four-step include – A includes B includes C includes D – template D will have a Scope that is six levels down. If you have this in template D:
My name is {{ name }}.
Here’s where it will look for the value of name
, in order.
- Scope for itself (Template D)
- Scope for Template C
- Scope for Template B
- Scope for Template A
- The Scope for the current
TemplateContext
- The Global scope on
TemplateOptions
- The Model
It will return the first instance of name
it finds. If gets though all six Scopes and finds nothing, it will look on the Model (more on that below). If it still doesn’t find anything, it will return NilValue
.
Remember, every template looks at its local Scope, then up through its ancestor Scopes. A template never looks down through its descendant Scopes.
Since a template returns the first value it finds, this means that a variable can be overridden by a child Scope, and every Scope below that will use the new value, while Scopes above it will be blissfully unaware of the change.
Let’s say you assign the name
variable like this:
context.SetValue("name", "Deane");
Then you have this templating code:
{{ name }}
{% include "B" %}
{{ name }}
{% assign name = "Annie" %}
{% include "C" %}
{{ name }}
Here’s the output:
Deane
Annie
Deane
This execution had five Scopes, and here’s where the value of name
is set.
- Global Scope on
TemplateOptions
: not set- Scope on
TemplateContext
: set to “Deane”- Scope on Template A: not set
- Scope on Template B: set to “Annie”
- Scope on Template C: not set
- Scope on Template B: set to “Annie”
- Scope on Template A: not set
- Scope on
Template A looked for name
and found it in the Scope of the Context. Template B set name
in its own Scope (it didn’t use the value, it just set the value). Then when Template C went looking, it didn’t find it locally, but found it one level “up” in B’s Scope. Then when execution of Template A resumed after the include, Template A again found it in the Scope of the Context with its original value, and had no idea it was ever changed.
Every time a new Scope is “entered” (inside the include
statement), a recursion counter is incremented. If this counter goes over TemplateOptions.MaxRecursion
, an exception is thrown.
Finally, as we noted, sitting “above” all Scopes is the Model. If all Scopes are exhausted and the value has not been found, the context will reflect the Model object (if it exists – it’s optional, remember) in an attempt to find the value. This is the “source of last resort.”
This leads us to an important point: Scopes take precedence over the Model. So, this is pointless –
var context = new TemplateContext(new { name = "Deane" });
context.SetValue("name", "Annie");
We set the value of name
via the Model, but then immediately “hid” it by setting a value for name
on the Context. When a template tries to find name
, it will find and use Annie
(in a Scope) before it ever gets back to Deane
(on the Model).
(Actually, the above is not totally pointless. You might have a Model that you didn’t explicitly construct – it was provided from some other code. Using SetValue
enables you to “modify” the Model by providing new values for some properties which might be read-only. To be clear, the exact code from above is pointless, but the concept it demonstrates is not.)