Sunday, September 18, 2011

Umbraco Razor Feature Walkthrough–Part 7

Welcome back!

Part 7 of the feature walkthrough continues with features available in umbraco 4.7.1
Today, I'm covering the new features available to help you write markup faster with less conditionals - if statements.

IsHelper

I like elegant code, especially in my Razor templates, and I found myself repeatedly having to write @foreach loops with a counter variable (e.g. int i = 0; i++) in order to track what node I was iterating over. In part 6, I introduced a new method called Index (aliased as position) which lets you get the current position.

This index property has been combined with the parent and children methods on DynamicNode to solve a lot of your standard requirements when iterating over nodes and generating HTML. Since generating HTML is the primary job of Razor, so we want it to be as concise and as quick as possible.

IsHelper (this is the internal name) methods all work the same, and have 3 overloads. They're basically ternary operators, but work a little nicer in that they're easy to embed in properties and quicker to write as you don't need so many brackets to make Razor understand them.
The general idea behind IsHelper is to allow you to dynamically inject class names and style attributes onto your nodes, based on their position within the list you're iterating. You can use this for a variety of things such as alternating row colours or fixing the margin/padding on the first/last item etc.

All DynamicNodes (except for the top most node) belong to a set, whether that's their parents list, or a special set created in context such as the result from Ancestors
This means that when you're iterating over a set, the node will belong to that set, no matter where the original node was sourced from in the tree.
What this means is that each node as you iterate has a concept of the list that contains it (that you are iterating) so a method call to that node can determine it's position in the iteration.

Here are the overloads:
IsHelper() => bool
IsHelper(string valueIfTrue) => valueIfTrue || emptyString
IsHelper(string valueIfTrue, string valueIfFalse) => valueIfTrue || valueIfFalse

IsHelper is just an internal name though, so when calling them, you use the name of the specific IsHelper you want.
These are:
IsFirst
If this is the first node in a set
IsNotFirst
If this is not the first node in a set
IsLast
If this is the last node in a set
IsNotLast
You guessed it! If this is not the last node in a set
IsPosition, IsNotPosition
Is the current node at index? Example: @item.IsPosition(3)
IsModZero, IsNotModZero
Is the current node position evenly dividable (modulus) by a given number? Will be true for every 3rd node: @item.IsModZero(3)
IsEven, IsOdd
Shorthand versions for IsModZero(2) and IsNotModZero(2)
IsEqual
Tests if the current node in your iteration is equivalent (by id) to another node

IsDescendant,IsDescendantOrSelf
Is the current node a descendant of another specified node. Would usually be used if your node set wasn't contigious, and came from a Descendants & Where for example
IsAncestor,IsAncestorOrSelf
Is the current node an ancestor of another specified node. Would usually be used if your node set wasn't contigious, and came from a Descendants & Where for example
The direction of this method is read like this: "Is the current node an ancestor of this node"

As all of these methods work the same way, I've only given you one example. Substitute the different helper names to see the result:

<ul> @foreach(var item in Model.Children) { <li style="color:@item.IsEven("red","blue")">@item.Name</li> } </ul> 


DynamicNode.Where

In a similar vein to IsHelper, DynamicNode.Where allows you to execute a predicate against the current node, and return either a bool, or valueIfTrue or valueIfFalse. The overloads are the same as IsHelper.
Here's an example:

<div class='@Model.Where("width > 200", "wide", "notwide")'> ... </div> 

[I]RazorDataTypeModel - Datatype Developers Property Data / Model interface

This is going to be a bit of a wordy one, but bear with me,

In 4.7.0 (or it could been after 4.7.0 for 4.7.1, I forget - trying to get my commit cape)
I added a special hardcoded up-cast (well I call it that, the bit of DynamicNode that converts strings to their types depending on their content) for the uComponents multi node tree picker.
I did this because I used it on a site and I thought, hey, wouldn't it be cool if the selected nodes came back as a List<int> instead of a CSV list?

Obviously, I wasn't thinking, because this didn't make sense; I wasn't going to add hard coded types for every datatype available, and as indisposable as uComponents is, it still shouldn't have
support for it's return types built into the core.

It wasn't until I was talking to Jeroen on the last day of Codegarden 2011, the developer of DAMP (a multiple media picker) who had just shown the beta of 2.0 that I realised that it might be
useful if datatype developers could provide a way for their data type to return an instance of a class instead of the underlying data from the property (which is always a string in v4 - this is what the
upcasting code does)

Enter IRazorDataTypeModel, excuse the name.. but it's a model for your data type for Razor, so it makes sense in context.
It works like this:
1) Data type developer or development team creates (and decorates) a class to use as the Model for their data type (or consumer if you're really keen)
2) The data type is added to a document type and used on a content item
3) The property data is accessed in a Razor script file with @Model.propertyName
4) The data type model class is returned instead of the underlying property data

Cool.. so if you're a data type developer, how do you use this?
I've intentionally tried to keep this simple, so that very little work is required by data type developers, yet performance should still be good.
To do this, I've created a custom attribute which is used to signify to DynamicNode that you have a model class for your plugin.
This attribute defines the GUID of your data type (which is defined in the Id property of your data type itself as well) and provides a way that the runtime can identify your class as a data type model,
and determine what editor control it's for, without instantiating that model class.
Your model class must have an empty constructor, but all .net classes have this by default, unless you define constructors, in which case you must define an parameter-less constructor.
The model class must also implement the IRazorDataTypeModel interface, which is a single method that DynamicNode uses to pass the property data through to your instance for the node.
This method returns the instance of your data model.

Right, so here's some sample code, this class will be returned for the DAMP media picker (the GUID you see below is for DAMP), but it's obviously not a complete example, just a proof of concept.
It's up to the data type developers to create their own implementations.

using System; using System.Collections.Generic; using System.Linq; using System.Text; using umbraco.MacroEngines; using System.Xml; namespace RazorDampModel { [RazorDataTypeModel("EF94C406-9E83-4058-A780-0375624BA7CA")] public class DampModel : IRazorDataTypeModel { public object PropertyData; public int CurrentNodeId; public XmlDocument AsXml; public bool Init(int CurrentNodeId, string PropertyData, out object instance) { this.PropertyData = PropertyData; this.CurrentNodeId = CurrentNodeId; AsXml = new XmlDocument(); AsXml.LoadXml(PropertyData.ToString()); instance = AsXml; return true; } } } 

You can see in this example, the GUID for DAMP being passed in the RazorDataTypeModel attribute, and the single required method from the IRazorDataType model interface, Init.

This method receives the current node id as an integer and the PropertyData as an string, and is expected to return an instance of the class in the "out object instance" variable (hint: you could return 'this' here, or an immutable type such as string, int)

Once this is done, that DLL can be deployed as part of the plugin OR compiled into a seperate assembly and dropped into the bin folder.
This means that if you use a data type that hasn't implemented this feature, you can write the model class for that data type, submit it to the data type developer, and use your implementation locally until it does get included.

For the example above, you'd be able to access the XmlDocument field labeled AsXml like so:

@Model.propertyName.AsXml.OuterXml 

This would of course return the outer XML of the XmlDocument to the razor template.
I can't wait to see how this feature gets used!

I've started a project on codeplex with a few examples, but they're just quick ones I hacked together when building a recent site:
http://razordatatypemodels.codeplex.com/



Conclusion

I've got quite a few more blog posts planned for the coming few days, introducing all the new features that are in umbraco 4.7.1

I'm Gareth Evans, Follow me at @agrath on twitter, and here's a few links:
The new Razor forum on our.umbraco: http://our.umbraco.org/forum/developers/razor
Codeplex for any feature requests or bugs: http://umbraco.codeplex.com/

Read more from the Umbraco Razor walkthrough series

Umbraco Razor Feature Walkthrough - Part 1
Umbraco Razor Feature Walkthrough - Part 2
Umbraco Razor Feature Walkthrough - Part 3
Umbraco Razor Feature Walkthrough - Part 4
Umbraco Razor Feature Walkthrough - Part 5
Umbraco Razor Feature Walkthrough - Part 6
Umbraco Razor Feature Walkthrough - Part 7
Umbraco Razor Feature Walkthrough - Part 8

Want to be updated on everything Umbraco?

Sign up for the Umbraco newsletter and get the latest news and special offers send directly to your inbox