Do-it-yourself ORM for Sitecore

Hello Khabrovites.

sitecore
Sitecore has little coverage on the hub, but this very functional (and expensive) CMS is quite popular with those who can afford it. However, people developing (and especially supporting) sites on sitecore often complain about the difficulty of modifying templates. So, simply renaming the template or one field can lead to unpredictable and, most importantly, difficult to diagnose and fix violations in the site. And they can get out only after a few months. In addition, the use of standard SiteKorov FieldRenderers makes it difficult to control the layout, which was critical in our case.

Why a bike?

There are solutions for generating classes based on templates (like trac.sitecore.net/CompiledDomainModel ), but they are not very convenient to use and do not eliminate the binding to the template structure, field names. The mentioned CompiledDomainModel requires regeneration of all models after any changes. It is also poorly suited for joint development (constant conflicts in the generated code), requires unique names for all templates, is tied to the templates and IDs, and generates monstrous code in one file (on one of the projects there were more than 60,000 lines and open it was not very fast in VS).


Our team was fortunate enough to develop a new site for sitecore 6.3, based on experience in supporting existing sites. I’ll emphasize right away that this will be a content site. Interesting functionality was required only in the admin panel and it has little connection directly with the sitecore.

Next about the shape of the wheels

It was decided to leave the link to the sitecore on sublayout, fasten the strong typing for templates and fields, store the names of the fields only in a single and predictable place.

The basis of all our wrapper classes for templates is class Template, whose main task is to check all existing Item templates and verify their names with the declared ones. For the class-template relationship, the DataContract attribute is used.

Note: hereinafter, the code is shortened to convey the main idea and readability

[DataContract(Name = "Base text page")]
public class BaseTextPage : Template
{...}

public class Template
{
    private readonly Item item;
    public Template(Item item)
    {
        var missedTemplates = GetMissedTemplates(item, this.GetType()); //тут мы читаем DataContract и проверяем валидность создания класса из данного Item-а.
        if (missedTemplates.Any())
        {
          ...                
            throw new InvalidDataException("Item is not of required template”); // с указанием что и где не хватает
        }
        this.item = item;
    }
…
}


In the same Template class, there are several useful functions for accessing template fields:

protected T GetField(string name, T @default = default(T))
{
          var dataType = typeof(T);
          var field = this.Item.Fields[name];
…
    //несколько конструкций вида:
    if (dataType == typeof(string))
        {
            if (string.IsNullOrEmpty(field.Value))
            {
                return @default;
            }
            return (T)(object)field.Value;
        }
        if (dataType == typeof(LinkField))
        {
            return (T)(object)new LinkField(field);
        }
        if (dataType == typeof(ImageField))
        {
            return (T)(object)new ImageField(field);
        }
… //etc for all field types
}
protected T GetFromField(string name) where T : Template
{
          var link = this.GetField(name);
          if (link != null && link.TargetItem != null)
          {
              return (T)Activator.CreateInstance(typeof(T), link.TargetItem);
          }
          return null;
 }
 protected T GetFromParent() where T : Template
 {
        if (this.Item == null || this.Item.Parent == null)
        {
            return default(T);
        }
        return (T)Activator.CreateInstance(typeof(T), this.Item.Parent);
}


The next step is accessing the fields. A well-known problem of site -site with the support of site-sites is the names of fields generously scattered throughout the project. Our task is to have one and only one field name in the project. Again we use standard attributes, now DataMember.

[DataContract(Name = "Base text page")]
public class BaseTextPage : Template
{
    [DataMember(Name = "Big text content")]
    public string Text
    {
        get
         {
             return this.Item[this.GetFieldName(x => x.QuestionText)];
         }
}
    [DataMember(Name = "Logo Image")]
    public string LogoImage
    {
        get
        {
            return this.GetField(this.GetFieldName(x => x.BigImage)).GetMediaUrl();
        }
    }
…
}


The most important thing here is the GetFieldName function, declared as an extension-method of the form:
private static readonly Dictionary fieldNameCache = new Dictionary();
public static string GetFieldName(this T obj, Expression> memberExpression) where T : class
{
    if (obj == null)
    {
        throw new ArgumentNullException("obj");
    }
    var member = memberExpression.ToMember();
    if (member.MemberType != MemberTypes.Property)
    {
        throw new ArgumentException("Not a property access", "memberExpression");
    }
    var fieldCahceKey = typeof(T).Name + member.Name;
    if (fieldNameCache.ContainsKey(fieldCahceKey))
    {
        return fieldNameCache[fieldCahceKey];
    }
    var fieldName = typeof(T)
          .GetProperty(member.Name)
          .GetCustomAttributes(typeof(DataMemberAttribute), true)
          .Cast()
          .Select(curr => curr.Name)
          .FirstOrDefault();
    if (string.IsNullOrEmpty(fieldName))
    {
        return null;
    }
    fieldNameCache[fieldCahceKey] = fieldName;
    return fieldName;
}
private static MemberInfo ToMember(
          this Expression> propertyExpression)
{
    if (propertyExpression == null)
    {
        throw new ArgumentNullException("propertyExpression");
    }
    var expression = propertyExpression.Body;
    if (expression.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = expression as MemberExpression;
        if (memberExpression != null)
        {
            return memberExpression.Member;
        }
    }
    throw new ArgumentException("Not a member access", "propertyExpression");
}


At this point we can write something like:

BaseTextPage page = new BaseTextPage(Sitecore.Context.Item);
var text = page.Text;
var imageUrl = page.LogoImage;


and get the data from the “Big text content” / ”Logo image” fields of the current Item, provided that its template is suitable for the BaseTextPage class.

Next, we make a wrapper for the template that will be the base for all our other templates. At least it will be a “Standart template”, but it is usually better to do something more useful. For example

[DataContract(Name = "Base page")]
public class BasePage : Template
{
    [DataMember(Name = "Show in menu")]
    public bool ShowInMenu
    {
      get
          {
              return this.Item[this.GetFieldName(x => x.ShowInMenu)].GetBoolValue();
          }
    }
    [DataMember(Name = "Page title")]
    public string Title
    {
        get
            {
                return this.Item[this.GetFieldName(x => x.Title)];
            }
    }
}


Now we implement all this in Sublayout:

public class BaseSublayout : UserControl
      where T : BasePage
{
    protected virtual T Model
    {
      get
          {
              return (T)Activator.CreateInstance(typeof(T), Sitecore.Context.Item);
          }
    }
}
public partial class ConcreteTextPage: BaseSublayout
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var smthUsefull = this.Model.HeaderText;
    }
}


From this moment, the contents of the .aspx files begin to resemble those in ASP.MVC. To enhance the convenience effect, a set of extension methods has been made to display markup with standard checks for the presence / validity of data (for example, do not display pictures with empty src or links without href).

<%= this.Model.Header %>

<%= HtmlHelper.RenderImage(this.Model.SomeEntity.MainImage) %> <% foreach (var link in this.Model.SelectedLinks) { %> <%= HtmlHelper.Anchor(link.Url, link.Text) %> <% } %>


Advantages of the approach:
+ all the necessary fields of the contextual Item at hand, in the form of properties
+ the contextual Item always has the correct template
+ all standard checks for data availability for a sitecore are made in one place
+ centralized access to site settings for which similar wrappers are written
+ clean (minimal, absent) code of pages
+ full control of layout
Cons
- all mapings by hand
- expenses for extracting field / template names

I hope this article will be useful to sitecore developers.

Also popular now: