Kentico 8 Technology - Macro improvements
Are you new to Kentico and want to learn how to make your website content more dynamic and perform different actions under different conditions? Or maybe you are an experienced Kentico developer and you are simply curious about what new and exciting things are available in the Macro engine in Kentico 8. Read on!
Macros were significantly refactored in Kentico 8 to bring you improvements in the following areas – stabilization, performance and easier customization.
Macros in Kentico 8
Three types of macros can be used in the new version:
- {% expression %} – standard data macro – main and preferred type, supports object-oriented language named K#
- {% parameter %} – query macro – returns values of query string parameters; anything allowed in K# language can also be used here (alternatively, you can get the values of query string parameters inside standard macro: {% QueryString.parameter %})
- {$ stringkey $} – localization macro – returns value of resource string only; K# language is not supported
There were more types of macros in previous versions. Their support was cancelled in Kentico 8 as they didn’t allow advanced operations, autocompletion, etc. Each of them has an equivalent form in standard data macros:
- {@ cookiename @} – cookie macro – use {% Cookies[“cookiename”] %} instead
- {& path &} – path macro – use {% Path[“path”] %} instead
- {# expression #} – custom macro – adding a custom method or property is preferred way (I will describe customization options in a separate chapter)
If you use previous types of macros in your project and you plan to upgrade to Kentico 8, there is nothing to worry about as those macro types are transformed automatically into standard data type.
The result of a macro expression can be modified via the macro parameters. The set of macro parameters was reduced in the new version and contains only parameters that change the context in which a macro expression is resolved. You can use the following parameters:
- |(encode) – encodes the result
- |(default) – returns the default value if the macro result is empty string
- |(culture) – determines the culture that is used for dates and numbers operations
- |(resolver) – specifies name of the resolver which is used to resolve the macro
- |(debug) – if the detailed debug is disabled globally (in the Settings->System->Debug), it can be allowed for the specific macro by this parameter
- |(casesensitive) – turns on case sensitive string comparison (false by default)
- |(timeout) – sets timeout for the macro resolve; resolving is cancelled if time exceeds the timeout limit
- |(handlesqlinjection) – replaces quotes (‘) with double quotes (‘’) to avoid SQL injection
- |(notrecursive) – if the value is set to true and the result is another macro, the result macro is not resolved
Macro resolvers and their data sources
Macro resolvers are system components that ensure the processing of macros. They are organized in a hierarchy that allows child resolvers to inherit all data and settings from the parent. The
Global resolver (CMS.MacroEngine.MacroContext.GlobalResolver) is the parent of all resolvers in the system. By adding fields into the global resolver, you create new macros that are available in the whole system.
If you want to add your own custom macros, Kentico 8 offers you two different options. You can register custom macros as a data source in macro resolvers; or, you can perform customization by extending existing objects or macro namespaces. First, I will explain the data sources.
The macro engine supports three main types of data sources –
anonymous data sources, named data sources, and
named callback source. Apart from these, there are three special types –
OnGetValue handler,
Namespaces and
Source aliases.
Anonymous data source
Content of the data source is registered, but not the source object. I will demonstrate this type on the DataRow – its columns can be used in macros directly if the DataRow is registered as an anonymous source:
// drData is a DataRow with columns Column1 and Column2
MacroResolver resolver = MacroResolver.GetInstance();
resolver.AddAnonymousSourceData(drData);
Now I can use columns from drData in macros – e.g. {
% Column1 %} (Column1 is a column of drData).
Named data source
Source data is registered into the macro resolver with a given name. Contrary to anonymous source, we cannot access the source properties directly, we reference the source object itself.
// drData is a DataRow with columns Column1 and Column2
MacroResolver resolver = MacroResolver.GetInstance();
resolver.SetNamedSourceData("MyData", drData);
Using this registration, values from drData columns will be returned by resolving macros like {
% MyData.Column1 %}. The macro from the previous example – {
% Column1 %} – will not be resolved!
Named data sources are handled prior to anonymous sources during resolving.
Named callback source
A callback method is used to get a value when the registered name occurs in a macro expression. The callback source can be registered by calling
SetNamedSourceDataCallback method on a resolver object:
// Registers a macro field, with the value processed by callback method
MacroContext.GlobalResolver.SetNamedSourceDataCallback("CallbackProperty", MacroPropertyEvaluator);
...
// Callback method that provides the value of the 'CallbackProperty' macro field
private object MacroPropertyEvaluator(EvaluationContext context)
{
// Returns the result of the macro
...
}
Namespaces
The macro engine now supports namespaces. Namespaces serve as containers for static macro methods and fields. We can access the namespace members when writing macro expressions starting with the namespace, for example {
% Math.Pi %}. Namespaces also appear in the macro autocomplete help. Besides Math (shown in the previous example), the system uses several other default namespaces such as String or Util. You can create your own namespaces for custom macros too.
Namespaces may look similar to the named data sources but they are not the same. I will show the difference between the two in the following example:
- Named data source – {% MyData.MyMethod() %} is equivalent to method call MyMethod(MyData), the source is passed to the method as the first parameter
- Namespace – {% MyNamespace.MyMethod() %} is equivalent to method call MyMethod()
Source alias
It is not exactly a source of data. The source alias “redirects” the resolver to another macro expression:
resolver.AddSourceAlias("User", "CMSContext.CurrentUser");
If a macro resolver doesn’t have “User” variable in currently resolved expression (dynamic source) or registered as a named or anonymous data source then it returns data from “CMSContext.CurrentUser”.
Order of the macro resolve
Data sources are resolved by a macro resolver in the following order:
- Dynamic sources (variables from currently resolved macro expression)
- Named data sources
- Anonymous data sources
- Namespaces
- Source aliases
- Data sources in the parent resolver
Customization
The system allows you to extend the macro engine in several ways. You can prepare custom macros that provide any functionality required by your users.
There are two ways how to customize default functionality:
- custom macro methods
- custom macro fields
It is also recommanded to organize your customizations under namespaces – the system ones may be extended or you can create your own.
Custom macro methods
We will start by defining the macro methods inside a macro container class.
- Create a new class with the reference to the CMS.MacroEngine
- The class inherits from the MacroMethodContainer
- Defined static methods should always have the following signature:
public static object MyMethod(EvaluationContext context, params object[] parameters)
The
EvaluationContext parameter allows you to get information about the context in which the macro containing the method was resolved, e.g. the culture or case sensitivity of string comparisons. The
parameters array stores the method's parameters.
- Specify each of the methods by the MacroMethod attribute with the following parameters:
- Type – the return type of the method
- Comment – comment displayed in the macro autocomplete help
- Minimum parameters - the minimum number of parameters that must be specified when calling the method
- For every parameter of the method the MacroMethodParam attribute must be declared having the following parameters:
- Index number – index of the parameter in the params array
- Name
- Data type
- Comment – appears in the macro autocomplete help
Here is the code example from previous steps:
/// <summary>
/// Wrapper class to provide new methods in the MacroEngine.
/// </summary>
class CustomMacroMethods : MacroMethodContainer
{
/// <summary>
/// Retrieves a substring from this instance.
/// </summary>
/// <param name="context">Evaluation context with child resolver</param>
/// <param name="parameters">Method parameters</param>
[MacroMethod(typeof(string), "Retrieves a substring from this instance.", 2)]
[MacroMethodParam(0, "text", typeof(string), "Base text.")]
[MacroMethodParam(1, "index", typeof(int), "The index of the start of the substring as the second.")]
[MacroMethodParam(2, "length", typeof(int), "The number of characters in the substring as the third.")]
public static object MySubstring(EvaluationContext context, params object[] parameters)
{
// Body of the method
}
}
After new methods in the container class are prepared, we need to register the container by extending a type or a macro namespace.
Add the
RegisterExtension assembly attribute above the class declaration for each type that you wish to extend (requires a reference to the CMS namespace) and use the following format:
[assembly: RegisterExtension(typeof(<macro method container class>), typeof(<extended type>))]
The following types can be extended:
- General system types (string, int, ...)
- Kentico API object types (UserInfo, TreeNode, ...)
- Macro namespaces (SystemNamespace, StringNamespace, MathNamespace, ...)
- Custom types
Example:
using CMS;
using CMS.MacroEngine;
// Makes all methods in the 'CustomMacroMethods' container class available for string objects
[assembly: RegisterExtension(typeof(CustomMacroMethods), typeof(string))]
// Registers methods from the 'CustomMacroMethods' container into the "String" macro namespace
[assembly: RegisterExtension(typeof(CustomMacroMethods), typeof(StringNamespace))]
public class CustomMacroMethods : MacroMethodContainer
{
...
Once we have new macro methods implemented and registered, we can call them in macro expressions.
Custom macro fields
In order to create a custom static field that can be used in an existing type or namespace, follow these steps:
- Create a new class with the reference to the CMS.MacroEngine
- The class inherits from MacroFieldContainer
- Override the RegisterFields method
- Call RegisterField to define your custom fields
Add a
RegisterExtension assembly attribute above the class declaration for each type that you wish to extend (requires a reference to the CMS namespace) and use the following format:
[assembly: RegisterExtension(typeof(<macro field container class>), typeof(<extended type>))]
The following types can be extended:
- General system types (string, int, ...)
- Kentico API object types (UserInfo, TreeNode, ...)
- Macro namespaces (SystemNamespace, StringNamespace, MathNamespace, ...)
- Custom types
Example:
using CMS;
using CMS.MacroEngine;
// Registers fields from the 'CustomMacroFields' container into the "System" macro namespace
[assembly: RegisterExtension(typeof(CustomMacroFields), typeof(SystemNamespace))]
class CustomMacroFields : MacroFieldContainer
{
protected override void RegisterFields()
{
base.RegisterFields();
RegisterField(new MacroField("PI", () => Math.PI));
}
}
Now the field's value can be accessed in macro expressions as a member of the extended object type or namespace.
Custom namespaces
Macro namespaces serve as containers for static macro methods and fields, for example
{% Math.Pi %} or
{% Math.Log(x) %}. They allow the fields and methods to be accessible via one central point in the autocomplete help. The system uses several default namespaces such as Math, String or Util, and it can be easily extended by your own namespaces for custom macros.
- Create a class inheriting from MacroNamespace<Namespace type>
- Register macro fields or methods into the namespace — add Extension attributes to the class, with the types of the appropriate container classes as parameters (please see the previous two chapters for more details about field/method container classes)
Example:
using CMS.Base;
using CMS.MacroEngine;
[Extension(typeof(CustomMacroFields))]
[Extension(typeof(CustomMacroMethods))]
public class CustomMacroNamespace : MacroNamespace<CustomMacroNamespace>
{
}
Once the macro namespace class is ready, we need to register the namespace as a source into a macro resolver (typically the global resolver).
The recommendation is registering your macro namespaces at the beginning of the application's lifecycle. Choose one of the following options:
- During the initialization process of the application itself — use the CMSModuleLoader partial class in the App_Code folder.
- When initializing custom modules — override the OnInit method of the module class.
The following example code shows how to register the custom namespace both as a named and as an anonymous source:
// Registers "CustomNamespace" into the macro engine
MacroContext.GlobalResolver.SetNamedSourceData("CustomNamespace", CustomMacroNamespace.Instance);
// Registers "CustomNamespace" as an anonymous macro source
MacroContext.GlobalResolver.AddAnonymousSourceData(CustomMacroNamespace.Instance);
UI for macro management and debugging
If you encounter problems when working with macro expressions, the system now provides more sophisticated tools that can help you identify and fix the issues. The central point for macro management, which can be used by administrators, is the Macros subsection in the System application. The section consists of the following parts:
- Debug
- Signatures
- Report
- Console
- Benchmark
I will describe each part separately.
Debug
The macro debug allows you to analyze how macros are resolved in the system. The debug can help you:
- confirm when and where your macros are being processed
- identify the exact source of problems
- detect syntax errors
The log displays a list of recent page requests, along with the macros that the system resolved while processing the requests.
The macro debug is disabled by default and it can be enabled in
Settings -> System -> Debug (Macros category). To get detailed information in Macro debug section you can allow
“Enable detailed macro debug” in the same Settings category. The macro debug is also accessible in the Debug application.
Signatures
For security reasons, nearly all saved macros are extended with security signature that contains user name of the macro’s author and a hash. To increase the level of security, a salt is added to the input via the hash function. The salt depends on the application’s environment. By default, a randomly generated GUID is used as the hash salt and it is set in the
CMSHashStringSalt key in the appSettings section of the web.config file. Instances without this web.config key use the application's main database connection string as the salt. We strongly recommend using a custom salt in the CMSHashStringSalt key and setting it before you start creating content for your website. This recommendation is especially important if you are about to use the Staging module, the same salt should be preset for all servers that are involved. Changing the salt causes all current hash values to become invalid.
If the hash salt value used by your application changes and the security signatures of existing macros become invalid, you can repair invalid signatures by re-signing all macros in the system. This operation can be done in
System -> Macros-> Signatures:
Report
In
System -> Macros -> Report you can find a report of all macro expression occurrences in your system. There are several filtering options to facilitate your search. To highlight one –
Report problems option – if checked the report displays only macros which are not syntactically correct or which signatures are not valid. Such macros can lead to problems on your websites.
Console and Benchmark
The Console and Benchmark dialogs provide you with an in-depth view of macro expressions. Here, you can examine your macros, check the results and test their performance.
Caching
Caching mechanism in macros allows you to boost performance if some of your macros have computationally intensive logic.
To cache the result of a macro, enclose the macro expression into the
Cache method (as the first parameter). Additionally, the following optional parameters may be specified:
- int cacheMinutes - the number of minutes for which the application cache stores the macro's result (default value is 10 minutes)
- bool condition - a boolean condition that must be fulfilled (true) in order to cache the macro's result
- string cacheItemName - the name of the cache key that stores the macro's result. If you set the same cache key name for multiple macros, the macros share the same cached value. The default name includes variables, such as the macro's text (including the signature), and the name and culture of the user viewing the page where the macro was resolved.
- string cacheItemNameParts - any number of parameters whose values are combined with the cacheItemName into the name of the cache key
- CMSCacheDependency cacheDependency - sets dependencies for the cache key (use the GetCacheDependency method to get the dependency object)
Example:
{% Cache(CurrentUser.GetFormattedUserName(), 5, true, "MacroCacheKey|UserName|" + CurrentUser.UserName, GetCacheDependency("cms.user|all")) %}
Summary
I hope you have enjoyed the article and have found interesting and helpful information. If you would like to learn more, please visit the Macros documentation pages -
https://docs.kentico.com/display/K8/Macro+expressions
Any feedback regarding Macros is highly appreciated. You can leave us a comment below or make a suggestion on ideas.kentico.com.