Using the ApplyTransformation macro
13th of June, 2016 0 comments

Using the ApplyTransformation macro

The first time I noticed ApplyTransformation listed in the Kentico macro reference, I was quite excited.

For any repetition of data, usually a repeater is used. Either standalone, or in conjunction with a data source webpart. If custom data needs to be retrieved, a query repeater or query data source is necessary. If this data needs any form of processing before being repeated, even more work is required - a custom data source webpart must be created. It takes time, and it's not much fun to build one.

Macros are cool - you can throw them anywhere, and they work. (Most of the time).

Imagine if it was possible to use a macro, instead of a custom data source + basic repeater combination?

Out of the box

Let's take a quick look at what Kentico says about the ApplyTransformation macro:

You can apply transformations of the Text/XML and HTML type to pages and other objects retrieved via macro expressions. Transformations in macro expressions allow you to display dynamically loaded data inside text and HTML content, where you cannot add listing web parts or controls.

They even threw in some examples, similar to the following:

{% SiteObjects.SKUs.Where("SKUDepartmentID = 4").ApplyTransformation("CMS.Root.ProductItem") %}

With the CMS.Root.ProductItem transformation being:

<div class="item">
    {% SKUName %} (${% SKUPrice %})
</div>

That's it! Your SKUs are rendered using the transformation you provided.

But what if we want to apply a transformation on a custom macro?

Taking it further

You can call the method for collections of objects that implement the IEnumerable interface, or for single instances of an object.

Okay, that sounds good. But it's not quite correct.

Let's give it a go. Here's a custom macro method, that returns a collection.

[assembly: RegisterExtension(typeof(MyCustomMacroContainer), typeof(string))]
public class MyCustomMacroContainer : MacroMethodContainer
{
	[MacroMethod(typeof(string), "Returns an IEnumerable from a string of comma-separated values", 1)]
    [MacroMethodParam(0, "param1", typeof(string), "String of comma-separated values")]
    public static object MyCustomMacro (EvaluationContext ctx, params object[] parameters)
    {
        // Let's simplify the switch-case example provided by Kentico, and only handle 1 parameter
        if (parameters.Length != 1)
            throw new NotSupportedException();
            
        var str = ValidationHelper.GetString(parameters[0], string.Empty);
        
        // Here's our IEnumerable
        return str.Split(',').AsEnumerable();
    }
}

For the transformation:

<div class="item">{% ... What should I use here? %}</div>

That won't work. We're iterating through our IEnumerable collection, and ApplyTransformation will loop through all items. But there is no macro method we could use in the transformation to render the current item. With the SKU example provided by Kentico, properties of the SKU object are defined as macros, so they can simply be accessed as {% SKUName %}, etc.

Fair enough. What if we return an object that contains properties? That might work.

[assembly: RegisterExtension(typeof(MyCustomMacroContainer), typeof(string))]

public class MyCustomObject
{
    public string MyValue { get; set; }
}

public class MyCustomMacroContainer : MacroMethodContainer
{
	[MacroMethod(typeof(string), "Returns an IEnumerable from a string of comma-separated values", 1)]
    [MacroMethodParam(0, "param1", typeof(string), "String of comma-separated values")]
    public static object MyCustomMacro (EvaluationContext ctx, params object[] parameters)
    {
        // Let's simplify the switch-case example provided by Kentico, and only handle 1 parameter
        if (parameters.Length != 1)
            throw new NotSupportedException();
            
        var str = ValidationHelper.GetString(parameters[0], string.Empty);
        
        // Here's our IEnumerable
        return str.Split(',').Select(x => new MyCustomObject { MyValue = x });
    }
}

This time, our macro returns an object of the IEnumerable<MyCustomObject> type.

This still does not work. Kentico simply does not pick up the properties in our custom object.

The solution

By implementing the IDataContainer interface on our MyCustomObject class, our fields will be accessible to the macro engine.

public class MyCustomObject : IDataContainer
{
	// The original property of our class
	public string MyValue { get; set; }
 	
    // List of column names, for macro intellisense
    public List<string> ColumnNames {
    	get {
        	return new List<string> { "MyValue" };
        }
    }
    
    // Not implemented for brevity
    public bool ContainsColumn(string columnName) {
    	throw new NotImplementedException();
    }
    
    // Not implemented for brevity
    public bool SetValue(string columnName, object value) {
    	throw new NotImplementedException();
    }
    
    public bool TryGetValue(string columnName, out object value) {
    	switch (columnName.ToLowerCSafe())
        {
        	case "myvalue":
            	value = MyValue;
                return true;
        }
        
        value = null;
        return false;
    }
    
    public object this[string columnName]
    {
    	get
    	{
    		return GetValue(columnName);
    	}
    	set
    	{
    		SetValue(columnName, value);
    	}
    }
    
    public object GetValue(string columnName)
    {
    	object value;
    	TryGetValue(columnName, out value);
		return value;
    }
}

And here it is in action:

Transformation

Macro result

Conclusion

ApplyTransformation is a very powerful tool, and can be used to render nested data in a neat way. No more clutter with foreach loops in transformations.

One important observation from the code above is that even if we have a simple collection of strings, we still need a class that implements IDataContainer. (The MyCustomObject class in the examples above might be a perfect candidate for this). It might sound a bit overkill at first, however it allows us to use a macro to render the currently iterated value.

Written by Kristian Bortnik



Comments