When Umbraco introduced the grid layout in Umbraco 7.2, and we (my colleagues and I at Skybrud.dk) started using the grid layout in some of our solutions, we quickly ran into some issues:
-
Dynamics
Since all of our developers working directly with Umbraco have a Visual Studio solution, we strive to always use a strongly typed approach (typed models) rather than using dynamics. However, the default Umbraco examples (eg. the Fanoe Starter kit) on how to render the grid all uses dynamics. -
Searching the grid
The value of a property with the Umbraco grid layout is saved as raw JSON (both in the database and in Examine). While this JSON is technically searchable, it's typically far from ideal to search through raw JSON using Examine.
As a result of these issues, my colleague tasked me with finding a solution for these, and thus Skybrud.Umbraco.GridData was born.
When you install a new instance of Umbraco, it comes with support for a number of grid editors by default. Skybrud.Umbraco.GridData provides strongly typed models for these, and this article will show how you can use these in your own projects. The article will also show how you can use Skybrud.Umbraco.GridData to provide strongly typed models for your own grid editors.
The Fanoe starter kit will be used for comparison. Also, the article is based on the newly released v2.0 of the Skybrud.Umbraco.GridData package. Most of the examples also apply to v1.5, but there are some differences.
To help with understanding the examples used throughout this article, I have also created an Umbraco sample solution - which amongst other things features a custom grid editor as well as a custom Examine indexer.
You can find the sample solution in this GitHub repository. The username and password for Umbraco are skrift and skrift1234 respectively. Just open the solution, build and then and press F5 ;)
Let's get to the basics
In default Umbraco, if you try to get the value of a grid property, the type will be an instance of JObject (representing the root JSON object of the grid value), which is great for working dynamics, but doesn't really give you a strongly typed model.
By installing our GridData package, the property value will instead be an instance of the GridDataModel class (don't worry - you can still use dynamics if you wish). So rather than:
dynamic grid = CurrentPage.content;
you can now get the grid value as:
GridDataModel grid = Model.Content.GetPropertyValue<GridDataModel>("content");
To be super fancy, you can also use an extension method instead:
GridDataModel grid = Model.Content.GetGridModel("content");
This extension method will make sure that you always get an instance of GridDataModel, and handle all the necessary null checking for you. Even if the property doesn't exist or the property isn't based on a grid layout, you will still get an instance of GridDataModel.
Getting the value of a control
With reference to the ~/Views/Partials/Grid/Elements/Base.cshtml partial view from the Fanoe starter kit, you have a reference to the control as dynamic (it's really an instance of JObject, but still referred to as a dynamic). To get the value of the control, you can simply write something like:
@inherits UmbracoViewPage<dynamic>
@{
dynamic value = Model.value;
}
This is quite simple, but you also don't have any intellisense. With the GridData package and a strongly typed approach, the model of your partial view would instead be an instance of GridControl. With this, you can get the value like:
@inherits UmbracoViewPage<Skybrud.Umbraco.GridData.GridControl>
@{
IGridControlValue value = Model.Value;
}
Here IGridControlValue is an interface used to represent the value of a grid control in a generic way. If we know the type of the control (eg. the alias of the parent grid editor), we can also get the value in another way:
@inherits UmbracoViewPage<Skybrud.Umbraco.GridData.GridControl>
@{
GridControlTextValue value = Model.GetValue<GridControlTextValue>();
}
However you should be aware that if you have a grid editor with an alias not supported by default, Model.Value will simply return null. This is because the GridData package doesn't know how to parse the value for a control using that editor. You can read more about handling this a bit later in this article.
Getting the configuration of an editor
In a similar way, you would refer to the configuration of a grid editor using dynamics like:
@inherits UmbracoViewPage<dynamic>
@{
dynamic config = Model.editor.config;
}
With the GridData package, the same code would instead look like:
@inherits UmbracoViewPage<Skybrud.Umbraco.GridData.GridControl>
@{
IGridEditorConfig control = Model.Editor.Config;
}
The IGridEditorConfig interface is likewise used to represent the configuration of grid editors in a generic way. For a text-based grid control, the concrete type can be retrieved like:
@inherits UmbracoViewPage<Skybrud.Umbraco.GridData.GridControl>
@{
GridEditorTextConfig control = Model.Editor.GetConfig<GridEditorTextConfig>();
}
Again, if the editor isn't supported by default, Model.Editor.Config will return null.
Traversing the grid
Again comparing with default Umbraco and the Fanoe starter kit, you could iterate over the grid sections, rows, areas and controls like:
<div class="grid">
@foreach (dynamic section in grid.sections) {
<div class="grid-section">
@foreach (dynamic row in section.rows) {
<div class="grid-row">
@foreach (dynamic area in row.areas) {
<div class="grid-area">
@foreach (dynamic control in area.controls) {
<div class="grid-control">@control.editor.alias</div>
}
</div>
}
</div>
}
</div>
}
</div>
With the GridData package, you can now iterate over the grid using the strongly typed models provided by the package:
<div class="grid">
@foreach (GridSection section in grid.Sections) {
<div class="grid-section">
@foreach (GridRow row in section.Rows) {
<div class="grid-row">
@foreach (GridArea area in row.Areas) {
<div class="grid-area">
@foreach (GridControl control in area.Controls) {
<div class="grid-control">@control.Editor.Alias</div>
}
</div>
}
</div>
}
</div>
}
</div>
If you're using Visual Studio, you'll now have full intellisense for the various properties and methods:
Also notice that even though we now have a strongly typed model, the GridDataModel class also exposes a section property (notice the lowercase s) so you can continue to use dynamics when and where you like.
The GridDataModel class and related classes support checking whether the entire model, section, row, area or control is valid.
Eg. the view for rendering the grid could look like below. If the grid model isn't valid (the entire grid is considered valid if it has at least one valid control), we stop any further rendering.
@using System.Web.Mvc.Html
@using Skybrud.Umbraco.GridData
@inherits UmbracoViewPage<GridDataModel>
@{
// Stop any further rendering since the model isn't valid
if (!Model.IsValid) { return; }
// TODO: Render the grid here
}
Rendering the grid
In default Umbraco, if you have to render the Grid, you're probably doing something like:
@CurrentPage.GetGridHtml("content", "fanoe")
In this example, content refers to the alias of the property holding the grid value, and fanoe is the template/framework used for the further rendering the grid, using the partial view located at ~/Views/Partials/Grid/Fanoe.cshtml.
With the GridData package, you can instead render the grid like:
@Html.GetTypedGridHtml(Model.Content, "content", "fanoe")
Internally this method will get the grid value as an instance of GridDataModel, which then will be provided as the model for the partial view located at ~/Views/Partials/TypedGrid/Fanoe.cshtml (notice that the folder name is now TypedGrid rather than just Grid).
Rendering a grid control
Grid controls are a little more complex to render. If we were to render a text-based grid control, we could pass a GridControl instance as model to a partial view, which could then look like:
@using Skybrud.Umbraco.GridData
@using Skybrud.Umbraco.GridData.Values
@using Skybrud.Umbraco.GridData.Config
@inherits UmbracoViewPage<GridControl>
@{
// Get the value of the control
GridControlTextValue value = Model.GetValue<GridControlTextValue>();
// Get the configuration of the parent editor
GridEditorTextConfig config = Model.Editor.GetConfig<GridEditorTextConfig>();
}
@if (config.HasMarkup) {
string markup = config.Markup;
markup = markup.Replace("#value#", value.ToString());
markup = markup.Replace("#style#", config.Style);
@Html.Raw(markup)
} else {
<div style="@config.Style">@value</div>
}
However if we for each partial view for a grid control were to get the value and the configuration, this would quickly seem to be a bit redundant. So for this purpose, the GridData package also features a GridControlWrapper class - which as the name suggests - is a wrapper for an instance of GridControl.
To get a wrapper instance for a text-based grid control, we could do something like (the first type parameter is the concrete type of the control value, while the second type parameter is the concrete type of the editor configuration):
GridControlWrapper<GridControlTextValue, GridEditorTextConfig> wrapper = control.GetControlWrapper<GridControlTextValue, GridEditorTextConfig>();
The first type parameter is the concrete type of the control value - in this case the GridControlTextValue class. The second type parameter is the concrete type of the editor configuration - in this case the GridEditorTextConfig. With the control wrapper used as the model for the partial view, it could instead look like:
@using Skybrud.Umbraco.GridData.Values
@using Skybrud.Umbraco.GridData.Config
@using Skybrud.Umbraco.GridData.Rendering
@inherits UmbracoViewPage<GridControlWrapper<GridControlTextValue, GridEditorTextConfig>>
@if (Model.Config.HasMarkup) {
string markup = Model.Config.Markup;
markup = markup.Replace("#value#", Model.Value.ToString());
markup = markup.Replace("#style#", Model.Config.Style);
@Html.Raw(markup)
} else {
<div style="@Model.Config.Style">@Model.Value</div>
}
With the updated partial view, Model.Value is a property of the type GridControlTextValue, and Model.Config is a property of the type GridEditorTextConfig. Keeps your view a bit more clean ;)
To "call" the partial view, you could do something like:
@Html.Partial("TypedGrid/Editors/Textstring", control.GetControlWrapper<GridControlTextValue, GridEditorTextConfig>())
Since the GridData package - through the added converters - knows about the type of the concrete type of the control value and the concrete type of the editor config, you can also just do something like:
@control.GetHtml(Html, "TypedGrid/Editors/Textstring")
@Html.RenderGridControl(control, "TypedGrid/Editors/Textstring")
Both methods here does exactly the same, so just pick the one that you prefer ;)
Extending the grid
Like I mentioned earlier, this package will only provide strongly typed models for the grid controls that comes with Umbraco by default (as well as the grid editors from the Fanoe starter kit). For these grid controls, you can get the value as:
IGridControlValue value = control.Value;
However if you have some custom grid controls, the control.Value property will simply return null because the GridData package doesn't know how to parse the JSON of the control values into strongly typed models. In a similar way, the control.Editor.Config property will also be null (it may also just be null because the editor doesn't have a configuration).
So to make these properties return strongly typed models, we need to use the IGridConverter interface. The grid package let's you add your own converter (or more than one if you'd like). As an example, the grid package comes with two converters by default - UmbracoGridConverter and FanoeGridConverter - which is why we already have strongly typed models for the default grid controls. You can have a look at these two converters for inspiration ;)
Anyways, the IGridConverter interface describes three methods, which I will try to describe with an example below.
My example is a grid editor for entering the details about one or more contact persons, where editors (the human kind logging into your backoffice) can specify the name, job title and email address of each contact person.
The raw JSON value of the control would look something like:
{
"title": "Contact persons",
"contacts": [
{
"name": "Nick Fury",
"title": "Director of SHIELD",
"email": "fury@shield.com"
},
{
"name": "Tony Stark",
"title": "Iron Man",
"email": "tony@stark.com"
},
{
"name": "Steve Rogers",
"title": "Captain America",
"email": ""
}
]
}
And the package.manifest would look like:
{
"gridEditors": [
{
"name": "Contact persons",
"alias": "SkriftContactPersons",
"view": "/App_Plugins/SkriftGridEditors/Views/ContactPersons.html",
"icon": "icon-users",
"config": {
"title": {
"show": true,
"placeholder": "Enter the title here",
"default": "Contact persons"
},
"limit": 15
}
}
]
}
So with that covered, let's head on to the three methods.
The examples below for the three methods comes from the same SkriftGridConverter class, which you can find in the sample solution.
ConvertControlValue
The ConvertControlValue method is used for converting the value of the grid controls.
The first parameter is an instance of the GridControl class - you can use this instance to check whether your converter actually should convert anything (whether it is a control that your converter supports).
The second parameter is an instance of JToken. The JToken class comes with the Newtonsoft.Json framework, and is a base class used to representing an arbitrary JSON value. If the control value is a JSON object, the parameter will really be an instance of JObject (which inherits from JToken), and the parameter will be an instance of JArray if the control value is a JSON array.
The third parameter is actually an output parameter. This means that if your converter supports a given control, you can pass the strongly typed model for the value back through this parameter.
The return type of the method is a boolean, which is used to indicate whether your converter supported the control. If it did, the grid package will skip checking with any further converters.
So for our example grid editor, the implementation of this method could look like:
public virtual bool ConvertControlValue(GridControl control, JToken token, out IGridControlValue value) {
// Just set the value to NULL initially (output parameters must be defined)
value = null;
// Check the alias of the grid editor
switch (control.Editor.Alias) {
// Handle any further parsing of the value of our grid control
case "SkriftContactPersons":
value = SkriftGridControlContactsPersonValue.Parse(control, token as JObject);
break;
}
// Return whether our converter supported the editor
return value != null;
}
The SkriftGridControlContactsPersonValue class - as shown in the example above - is used to represent the JSON that is saved by the grid editor.
ConvertEditorConfig
In a similar way, the ConvertEditorConfig method is used for converting the JSON configuration of the grid editor into a strongly typed method:
public virtual bool ConvertEditorConfig(GridEditor editor, JToken token, out IGridEditorConfig config) {
// Just set the value to NULL initially (output parameters must be defined)
config = null;
// Check the alias of the grid editor
switch (editor.Alias) {
// Handle any further parsing of the value of our grid editor configugration
case "SkriftContactPersons":
config = SkriftGridEditorContactPersonsConfig.Parse(editor, token as JObject);
break;
}
// Return whether our converter supported the editor
return config != null;
}
In this example, the SkriftGridEditorContactPersonsConfig class is used to represent the JSON of the grid editor configuration. Some grid editors doesn't have a configuration, so in that case you can simply skip the switch statement.
GetControlWrapper
In order to support the strongly typed models of the partial views for each grid editor, the grid package features a GridControlWrapper class.
If you see the example below, a new wrapper will be initialized for our example grid editor. The generics make sure that if the wrapper instance is passed as model to a partial view, we can use Model.Value for the strongly typed model of the control value, and Model.Config for the strongly typed model of the editor configuration.
public virtual bool GetControlWrapper(GridControl control, out GridControlWrapper wrapper) {
// Just set the value to NULL initially (output parameters must be defined)
wrapper = null;
// Check the alias of the grid editor
switch (control.Editor.Alias) {
// Initialize a control wrapper with the correct generic types
case "SkriftContactPersons":
wrapper = control.GetControlWrapper<SkriftGridControlContactPersonsValue, SkriftGridEditorContactPersonsConfig>();
break;
}
// Return whether our converter supported the editor
return wrapper != null;
}
Implementing the model for a control value
In general, the values of grid controls should implement the IGridControlValue interface. This interface describes two properties and a single method:
- Control
Since an instance of IGridControlValue represents the value of a specific grid control, the Control property is used for keeping a reference back to that control (which is an instance of the GridControl class). - IsValid
This property should return a boolean value whether the control and its value should be considered valid. Eg. if a control value represents a list of some kind, IsValid could return false if the list is empty since it then wouldn't make sense to render the control in the frontend. - GetSearchableText
To make Examine indexing and searching easier, each instance of IGridControlValue should be able to provide a textual representation of the value. For our example grid editor, this could be the name and job title of each contact person.
While both the IsValid property and the GetSearchableText method can be useful, there are also use cases where you don't really need them. So rather than implementing the IGridControlValue interface, you can also inherit from the GridControlValueBase class. This is an abstract class that implements the interface for you, so you don't have to implement the interface, but instead let's you overwrite the parts that you need to change.
So for the value of our example grid editor, the SkriftGridControlContactPersonsValue class could be implemented like:
using System;
using Newtonsoft.Json.Linq;
using Skybrud.Essentials.Json.Extensions;
using Skybrud.Umbraco.GridData;
using Skybrud.Umbraco.GridData.Values;
namespace SkriftGridDemo.Grid.Value.ContactPersons {
public class SkriftGridControlContactPersonsValue : GridControlValueBase {
#region Properties
/// <summary>
/// Gets the title of the control value.
/// </summary>
public string Title { get; private set; }
/// <summary>
/// Gets whether the control value has a title.
/// </summary>
public bool HasTitle {
get { return !String.IsNullOrWhiteSpace(Title); }
}
/// <summary>
/// Gets an array of the items.
/// </summary>
public SkriftContactPersonItem[] Items { get; private set; }
/// <summary>
/// Gets whether the control value has any items.
/// </summary>
public bool HasItems {
get { return Items.Length > 0; }
}
/// <summary>
/// Gets wether the control value is valid (AKA whether the value has any items).
/// </summary>
public override bool IsValid {
get { return HasItems; }
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance based on the specified <paramref name="control"/> and <paramref name="obj"/>.
/// </summary>
/// <param name="control">An instance of <see cref="GridControl"/> representing the control.</param>
/// <param name="obj">An instance of <see cref="JObject"/> representing the value of the control.</param>
protected SkriftGridControlContactPersonsValue(GridControl control, JObject obj) : base(control, obj) {
Title = obj.GetString("title");
Items = obj.GetArrayItems("items", SkriftContactPersonItem.Parse);
}
#endregion
#region Static methods
/// <summary>
/// Parses the specified <paramref name="obj"/> into an instance of <see cref="SkriftGridControlContactPersonsValue"/>.
/// </summary>
/// <param name="control">The parent control.</param>
/// <param name="obj">The instance of <see cref="JObject"/> to be parsed.</param>
/// <returns>An instance of <see cref="SkriftGridControlContactPersonsValue"/>.</returns>
public static SkriftGridControlContactPersonsValue Parse(GridControl control, JObject obj) {
return obj == null ? null : new SkriftGridControlContactPersonsValue(control, obj);
}
#endregion
}
}
Implementing the model for an editor configuration
This works in a similar way as with the control value. A strongly typed model for an editor configuration must implement the IGridEditorConfig interface.
The interface only describes a single property - that is the Editor property, which is used for referencing the parent GridEditor instance.
So again for our example grid editor, the implementation of the SkriftGridEditorContactPersonsConfig class would look like:
using Newtonsoft.Json.Linq;
using Skybrud.Essentials.Json.Extensions;
using Skybrud.Umbraco.GridData;
using Skybrud.Umbraco.GridData.Interfaces;
using Skybrud.Umbraco.GridData.Json;
namespace SkriftGridDemo.Grid.Config.ContactPersons {
public class SkriftGridEditorContactPersonsConfig : GridJsonObject, IGridEditorConfig {
#region Properties
/// <summary>
/// Gets a reference to the parent editor.
/// </summary>
public GridEditor Editor { get; private set; }
public SkriftGridEditorContactPersonsConfigTitle Title { get; private set; }
/// <summary>
/// Gets the maximum allowed amount of items.
/// </summary>
public int Limit { get; private set; }
#endregion
#region Constructors
private SkriftGridEditorContactPersonsConfig(GridEditor editor, JObject obj) : base(obj) {
Editor = editor;
Title = obj.GetObject("title", SkriftGridEditorContactPersonsConfigTitle.Parse) ?? SkriftGridEditorContactPersonsConfigTitle.Parse(new JObject());
Limit = obj.GetInt32("limit");
}
#endregion
#region Static methods
/// <summary>
/// Gets an instance of <see cref="SkriftGridEditorContactPersonsConfig"/> from the specified
/// <paramref name="obj"/>.
/// </summary>
/// <param name="editor">The parent editor.</param>
/// <param name="obj">The instance of <see cref="JObject"/> to be parsed.</param>
/// <returns>An instance of <see cref="SkriftGridEditorContactPersonsConfigTitle"/>.</returns>
public static SkriftGridEditorContactPersonsConfig Parse(GridEditor editor, JObject obj) {
return obj == null ? null : new SkriftGridEditorContactPersonsConfig(editor, obj);
}
#endregion
}
}
Indexing the grid in Examine
When having a property using the Umbraco grid layout, the property value is saved as raw JSON to the database, and also indexed in Examine as raw JSON. This JSON is technically searchable, but might also contain phrases that you don't want to be searchable - eg. the JSON property names.
Since we now have a strongly typed model for the entire grid value, we can also use this for Examine indexing. If you saw the IGridControlValue interface earlier, you probably also saw the GetSearchableText method.
With this method, each control can return the exact text that should be indexed for that control. Eg. GetSearchableTextmethod in the GridControlHtmlValue class strips all HTML elements, but still returns the inner text of said HTML elements. Similarly the GridDataModel class also has a GetSearchableText method, which is used to sum up the entire searchable text that should be indexed in Examine.
So an indexer for the grid could look like the example below (the SkriftGridExamineIndexer class can also be found in the sample solution). Also, remember to register the indexer with Umbraco during startup.
using System;
using Examine;
using Examine.Providers;
using Skybrud.Umbraco.GridData;
using Umbraco.Core.Logging;
namespace SkriftGridDemo.Grid.Indexers {
public class SkriftGridExamineIndexer {
private SkriftGridExamineIndexer() {
BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];
externalIndexer.GatheringNodeData += OnExamineGatheringNodeData;
}
public static SkriftGridExamineIndexer Init() {
return new SkriftGridExamineIndexer();
}
private void OnExamineGatheringNodeData(object sender, IndexingNodeDataEventArgs e) {
try {
string nodeTypeAlias = e.Fields["nodeTypeAlias"];
if (nodeTypeAlias == "Home" || nodeTypeAlias == "LandingPage" || nodeTypeAlias == "TextPage" || nodeTypeAlias == "BlogPost") {
string value;
// Just return now if the "content" field wasn't found
if (!e.Fields.TryGetValue("content", out value)) return;
// Parse the raw JSON into an instance of "GridDataModel"
GridDataModel grid = GridDataModel.Deserialize(e.Fields["content"]);
// Get the searchable text (based on each control in the grid model)
e.Fields["content"] = grid.GetSearchableText();
}
} catch (Exception ex) {
// Remember to change the message added to the log. My colleagues typically doesn't,
// so I occasionally see "MAYDAY! MAYDAY! MAYDAY!" in our logs :(
LogHelper.Error<SkriftGridExamineIndexer>("MAYDAY! MAYDAY! MAYDAY!", ex);
}
}
}
}
While this approach is great at minimizing your code, it's also very generic. There might be situations where you might need to customize the indexing a bit further.
If this is the case, you can iterate over the controls manually, and then concatenate the searchable string your self. You can see this GIST for further information (I tend to not see replies in my Gists, so feel free to create an issue or contact me on Twitter instead if you have any questions).
Using the GridData package in your own packages
If you have created your own grid editor and released it as a package, you can also provide strongly typed models for your package using the GridData package.
At Skybrud.dk we have a few packages that does this - you can find them through the links below:
Feel free to use them for inspiration ;)
Documentation
Good documentation is always a good starting point for understanding something - but good documentation also takes time to write.
So for the time being, the Skybrud.Umbraco.GridData documentation mostly contains the same examples as used in this article, but if you get stuck in the future, have a look at the documentation, since it hopefully has improved then.
Otherwise if you have any questions, feel free to post them in the comments below this article or create a new thread on Our Umbraco ;)