📜 ⬆️ ⬇️

ORM for Sitecore do it yourself

Hello habrovchane.

sitecore
Sitecore little coverage in the Habré, but it is 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 a template or a single field can lead to unpredictable and, most importantly, difficult to diagnose and correct violations of the site. And they can get out only after a few months. In addition, the use of standard Sitekor FieldRenderer-s makes it difficult to control the markup, which was critical in our case.

Why a bicycle?

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 binding to the template structure, field names. The mentioned CompiledDomainModel requires the 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 on the path to templates and ID-shki and generates monstrous code in one file (on one of the projects there were more than 60,000 lines and open his VS was not a very quick affair).
')

Our team was lucky to develop a new site for 6.3 sitecaster, based on the experience of supporting existing sites. Immediately emphasize that we will talk about the content site. An interesting functionality was required only in the admin panel and it has little connection directly with the sitecore.

Further on the shape of the wheels

It was decided to move away from binding to sitecore on sublayouts, to fasten strict typing for templates and fields, to store the names of the fields only in a single and predictable place.

The basis of all our classes for the templates is the class Template, whose main task is to check all existing Item templates and compare their names with the declared ones. For communication, the template class uses the DataContract attribute.

Note: here and hereafter the code is abbreviated 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<T>(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<T>(string name) where T : Template { var link = this.GetField<ReferenceField>(name); if (link != null && link.TargetItem != null) { return (T)Activator.CreateInstance(typeof(T), link.TargetItem); } return null; } protected T GetFromParent<T>() 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 to access the fields. A well-known problem of website with the support of website sites are the names of the 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<ImageField>(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<string, string> fieldNameCache = new Dictionary<string, string>(); public static string GetFieldName<T, TResult>(this T obj, Expression<Func<T, TResult>> 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<DataMemberAttribute>() .Select(curr => curr.Name) .FirstOrDefault(); if (string.IsNullOrEmpty(fieldName)) { return null; } fieldNameCache[fieldCahceKey] = fieldName; return fieldName; } private static MemberInfo ToMember<TMapping, TReturn>( this Expression<Func<TMapping, TReturn>> 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 stage we can write something like:

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


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

Next, we make a wrapper for the template that will be the base for all our other templates. At a minimum, this will be the “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 Sublayouts:

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


From this point on, the contents of the .aspx files begin to resemble those in ASP.MVC. To enhance the convenience effect, a set of extension-methods was used to display markup with standard checks for the presence / validity of data (for example, do not display images with empty src or links without href).
 <h1><%= this.Model.Header %></h1> <%= 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 context Item are at hand, in the form of properties
+ contextual Item always has the right template
+ All standard site control checks for data availability are done in one place.
+ centralized access to the site settings for which similar wrappers are written
+ clean (minimum, missing) page code
+ full layout control
Minuses
- all mapping hands
- the cost of extracting the names of fields / templates

I hope this article will be useful for sitecore developers.

Source: https://habr.com/ru/post/154313/


All Articles