Quantcast
Viewing all articles
Browse latest Browse all 43

Combining Sitecore ItemBinding with Glass.Mapper.Sc

You can’t have your cake and eat it too a popular saying goes. However I never like being told what I can and can’t do and the other day I was thinking about how cool it would be to combine something like the awesome encapsulation and automatic filed mapping from Glass.Mapper.Sc with the data validation and binding contracts from Sitecore ItemBinding into one mechanism?

Also I have had a lot of questions about Sitecore ItemBinding and why it doesn’t really do more when it comes to model class generation or encapsulation.

Well let me start by answering the last question first. Sitecore ItemBinding is all about automating and standardizing the item - model binding mechanism and providing data validation through extendable binding contracts. However I have realized that when it comes to how the model classes, POCOs, DTOs or what you like to call them are constructed and what level of abstraction and encapsulation that goes into them just about every developer has his or her own opinion about what is right and wrong and I wanted to create something that was universally beneficial without any specific requirements for the model. If you want to use TDS T4 templates to generate your model classes (yes I know I promised that I would make some and hopefully they will be ready soon) or make something in Sitecore or Sitecore Rocks that can generate your model then all power to you. On the same notion the decision of how to treat the Sitecore data API and if it should be encapsulated or not is up to you. With that said I guess it also pretty much answers the first part about why I would try to combine Glass.Mapper.Sc with Sitecore ItemBinding - simply because it is the best way to show that Sitecore ItemBinding can be used with pretty much any kind of model class mechanism - ohh that and because I like eating cake.

So without further ado – here is a PoC on combining Glass.Mapper.Sc with sitecore ItemBinding.

First of all the ItemBinding works through a ModelFactory that is responsible for binding items to model classes so we will need one of those.

using System;
using Glass.Mapper.Sc;
using PT.Framework.ItemBinding.Model;
using Sitecore.Data.Items;

namespace PT.Framework.ItemBinding.Glass.Model
{
  public class GlassModelFactory : ModelFactory
  {
    public override T Create<T>(Item item)
    {
      // Using BindingContract.Assert from Sitecore ItemBinding
      BindingContract.Assert<T>(item);

      // Using GlassCast from Glass.Mapper.Sc
      return item.GlassCast<T>();
    }

    public override Object Create(Item item, Type type)
    {
      // Using BindingContract.Assert from Sitecore ItemBinding
      BindingContract.Assert(item, type);

      // Using SitecoreService.CreateType from Glass.Mapper.Sc
      SitecoreService sitecoreService = new SitecoreService(item.Database);
      return sitecoreService.CreateType(type, item, false, false, null, new Object[0]);
    }
  }
}

This is the first version of the model factory and there are a few things to note. First of all I am using Sitecore ItemBinding and BindingContracts to assert that the item complies with the claims set by the contract. In this case I am simply using the standard AttributeBasedBindingContract, that works through attributes set on the model class. Secondly I am using Glass.Mapper.Sc to do the actual model class construction.

Now Sitecore ItemBinding gives you the possibility to specify a concrete ModelFactory at the time when you are creating the model class and binding the item to the model. But for this example we want to replace the default ModelFactory with our GlassModelFactory entirely and the easiest way to do that is at runtime to ensure that everything is ready. You can do that in a ton of ways both through the Global.asax application start method or using the Sitecore initialize pipeline. However I like to use WebActivatorEx which is available through NuGet and add an assembly attribute to my AssemblyInfo.cs file.

[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PT.GlassItemBindingExamples.Infrastructure.ModelFactoryInitialization), "Initialize")]

And that will fire my static Initialize method

using PT.Framework.ItemBinding.Application;
using PT.Framework.ItemBinding.Glass.Model;

namespace PT.GlassItemBindingExamples.Infrastructure
{
  public class ModelFactoryInitialization
  {
    public static void Initialize()
    {
      ModelFactoryService.SetPrototype(new GlassModelFactory());
    }
  }
}

The only thing to note here is that I am setting the prototype ModelFactory to a configured instance of the GlassModelFactory and each time the ItemBinding needs a ModelFactory it will clone a fresh copy from this prototype.

With the GlassModelFactory in place we can now focus on building some model classes. For this simple PoC I have chosen to create a simple news model and a news list model to hold a collection of news.

The GlassNews model class looks like this.

using System;
using Glass.Mapper.Sc.Configuration.Attributes;
using PT.Framework.ItemBinding.Model.BindingContracts;

namespace PT.GlassItemBindingExamples.Model
{
  [RequiredBaseTemplate("{7F97671B-E1CA-4537-823C-230A7EAC4DED}")]
  [RequiredField("{82B7DEB1-293A-49F4-8434-57B9B62F420F}")]
  public class GlassNews
  {
    [SitecoreId]
    public virtual Guid Id { get; set; }

    public virtual String Title { get; set; }

    public virtual String Teaser { get; set; }

    public virtual String Text { get; set; }

    [SitecoreField("__Created")]
    public virtual DateTime Created { get; set; }

    public virtual String Url { get; set; }
  }
}

The thing to note here is that it is a standard Glass.Mapper.Sc model class with the addition of two BindingContract attributes from Sitecore ItemBinding that is used to determine whether an item complies with the specified requirements by the BindingContract. In this case the requirements are that the item has to inherit from a specific template and that the field with the matching ID (the title field in this case) has to contain a value./p>

The GlassNewsList model class looks like this – well actually this is the first draft.

using System;
using Glass.Mapper.Sc.Configuration.Attributes;
using PT.Framework.ItemBinding.Model.BindingContracts;

namespace PT.GlassItemBindingExamples.Model
{
  [RequiredBaseTemplate("{ECC0A69D-F1B7-41E5-A4FC-83E4B086D995}")]
  public class GlassNewsList
  {
    [SitecoreId]
    public virtual Guid Id { get; set; }

    public virtual String Title { get; set; }
  }
}

In the same way as the GlassNews model class this is just a standard Glass.Mapper.Sc model class with an added BindingContract attribute. However the most important thing to note is that it doesn’t contain a collection of GlassNews elements and this is where I encountered my first challenge.

Usually in Glass.Mapper.Sc a collection of child items would be populated either by adding a property with the name Children to the model class or decorating a property with the [SitecoreChildren] attribute. However if we do that the child items will be bound though Glass.Mapper.Sc and not through our GlassModelFactory and therefore we would not get the benefits of the data validation from the ItemBinding framework. So I had to device a way to do the same kind of automatic child binding in the GlassModelFactory as is done in Glass.Mapper.Sc to be able to run the BindingContract assertions to ensure that the child items are valid before binding them. Now I do apologize for the following code which uses reflection in ways that reflections should never be used (not even in anger) however since this is only a PoC it will serve it’s purpose.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Glass.Mapper.Sc;
using PT.Framework.ItemBinding.Model;
using Sitecore.Data.Items;

namespace PT.Framework.ItemBinding.Glass.Model
{
  public class GlassModelFactory : ModelFactory
  {
    public override T Create<T>(Item item)
    {
      // Using BindingContract.Assert from Sitecore ItemBinding
      BindingContract.Assert<T>(item);

      // Using GlassCast from Glass.Mapper.Sc
      T model = item.GlassCast<T>();

      PopulateChildCollections(model.GetType(), item);

      return model;
    }

    public override Object Create(Item item, Type type)
    {
      // Using BindingContract.Assert from Sitecore ItemBinding
      BindingContract.Assert(item, type);

      // Using SitecoreService.CreateType from Glass.Mapper.Sc
      SitecoreService sitecoreService = new SitecoreService(item.Database);
      Object model = sitecoreService.CreateType(type, item, false, false, null, new Object[0]);

      PopulateChildCollections(model.GetType(), item);

      return model;
    }

    private void PopulateChildCollections(Object model, Item item)
    {
      Type modelType = model.GetType();
      IEnumerable<PropertyInfo> propertyInfos = modelType.GetProperties().Where(propertyInfo => Attribute.IsDefined(propertyInfo, typeof(ChildBindingAttribute)));

      SitecoreService sitecoreService = new SitecoreService(item.Database);
      foreach (PropertyInfo propertyInfo in propertyInfos)
      {
        Type[] genericArguments = propertyInfo.PropertyType.GetGenericArguments();
        Type genericListType = typeof(List<>).MakeGenericType(genericArguments);
        Object genericList = Activator.CreateInstance(genericListType);

        // Using BindingContract.IsCompliant from Sitecore ItemBinding
        IEnumerable<Item> children = item.GetChildren().Where(child => BindingContract.IsCompliant(child, genericArguments[0]));
        foreach (Item child in children)
        {
          // Using SitecoreService.CreateType from Glass.Mapper.Sc
          Object instance = sitecoreService.CreateType(genericArguments[0], child, false, false, null, new Object[0]);
          genericListType.GetMethod("Add").Invoke(genericList, new[] { instance });
        }
        propertyInfo.SetValue(model, genericList);
      }
    }
  }
}

The only thing to note is that I chose to introduce a [ChildBinding] property attribute much like the [SitecoreChildren] attribute from Glass.Mapper.Sc to identify the property to populate with the child collection. And yes I am very much aware of the fact that there is absolutely no error handling or type checking to ensure that the property is of the right type – I simply assume that it is an IEnumerable where T is the type of the model class that the child items should be bound to.

Now with the child binding in place we can finish the GlassNewsList model class.

using System;
using System.Collections.Generic;
using Glass.Mapper.Sc.Configuration.Attributes;
using PT.Framework.ItemBinding.Glass.Model;
using PT.Framework.ItemBinding.Model.BindingContracts;

namespace PT.GlassItemBindingExamples.Model
{
  [RequiredBaseTemplate("{ECC0A69D-F1B7-41E5-A4FC-83E4B086D995}")]
  public class GlassNewsList
  {
    [SitecoreId]
    public virtual Guid Id { get; set; }

    public virtual String Title { get; set; }

    [ChildBinding]
    public virtual IEnumerable<GlassNews> News { get; set; } 
  }
}

The only thing to note is the added News property with the type IEnumerable and the attribute [ChildBinding].

So now that we have our two model classes and the means to bind them to valid Sitecore items with automatic mapping of the model properties it is time to focus on using the models by creating a couple of user controls to display the data and of course they have to support page editing.

First up let’s look at what is available in Glass.Mapper.Sc and the ItemBinding framework. Well they both have abstract user controls with the GlassUserControl and the ModelBoundUserControl that we can derive our user controls from. The only problem is that they don’t inherit from each other and we can only inherit from one of them (this is one of the cases where you disagree with MS’ decision to remove multiple inheritance from .NET). Preferable I would like to have the automatic data/context item binding mechanism and the handling of cases where the model is null from the ItemBinding framework. However if I want to support page editing as a minimum I also need the Editable methods that are on the GlassUserControl which is the only way to make our Glass.Mapper.Sc models page editable and I have a feeling that there are other equally important bits and pieces in the GlassUserControl or some of the underlying base classes that we might need as well so in the end I simply decided to inherit from the GlassUserControl and copy all of the code from the ModelBoundUserControl into my own UserControl to create a new hybrid of the two (yes it was a real Frankenstein “It’s alive!!!” kind of moment - but hey it’s a PoC).

With my new hybrid GlassModelBoundUserControl in existence I made an override on the GetModel method from the GlassUserControl to ensure that all model creation went through the ItemBinding framework and the GlassModelFactory before being passed to the underlying GlassUserControl. That led to the following horrific GlassModelBoundUserControl.

using System;
using System.Web.UI;
using Glass.Mapper.Sc.Web.Ui;
using PT.Framework.ItemBinding.Application;
using PT.Framework.ItemBinding.Model;
using PT.Framework.ItemBinding.Presentation;
using PT.Framework.SitecoreExtensions;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace PT.Framework.ItemBinding.Glass.Presentation
{
  public abstract class GlassModelBoundUserControl<T> : GlassUserControl<T> where T : class
  {
    protected override void GetModel()
    {
      Model = ModelFactory.Create<T>(SourceItem);
    }

    /// <summary>
    /// Gets the model factory that is used to bind the SourceItem to the model class T.
    /// </summary>
    /// <value>The model factory that is used to bind the SourceItem to the model class T.</value>
    protected virtual IModelFactory ModelFactory
    {
      get { return _modelFactory ?? (_modelFactory = ModelFactoryService.GetPrototypeClone()); }
    }

    /// <summary>
    /// Gets the source item that is bound to the model class T exposed by the Model member.
    /// </summary>
    /// <value>The source item that is bound to the model class T exposed by the Model member.</value>
    protected virtual Item SourceItem
    {
      get { return _sourceItem ?? (_sourceItem = this.GetDataSourceOrContextItem()); }
    }

    /// <summary>
    /// Gets a value indicating whether the data source unpublishable information text should be shown.
    /// </summary>
    /// <value>
    /// <c>true</c> if the data source unpublishable information text should be shown; otherwise, <c>false</c>.
    /// </value>
    protected virtual Boolean ShowDataSourceUnpublishableInfoText
    {
      get { return true; }
    }

    /// <summary>
    /// Gets the information text that is displayed if the data source is unpublishable.
    /// </summary>
    /// <value>The information text that is displayed if the data source is unpublishable.</value>
    protected virtual String DataSourceUnpublishableInfoText
    {
      get { return "Datakilde kan ikke publiceres"; }
    }

    /// <summary>
    /// Gets a value indicating whether the data source unavailable information text should be shown.
    /// </summary>
    /// <value>
    /// <c>true</c> if the data source unavailable information text should be shown; otherwise, <c>false</c>.
    /// </value>
    protected virtual Boolean ShowDataSourceUnavailableInfoText
    {
      get { return true; }
    }

    /// <summary>
    /// Gets the information text that is displayed if the data source is unavailable.
    /// </summary>
    /// <value>The information text that is displayed if the data source is unavailable.</value>
    protected virtual String DataSourceUnavailableInfoText
    {
      get { return "Datakilde er ikke tilgængelig"; }
    }

    /// <summary>
    /// Gets a value indicating whether the control should be automatically databound on init.
    /// </summary>
    /// <value>
    /// <c>true</c> if the control should be automatically databound on init; otherwise, <c>false</c>.
    /// </value>
    protected virtual Boolean DatabindOnLoad { get { return true; } }

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.DataBinding" /> event.
    /// </summary>
    /// <param name="e">An <see cref="T:System.EventArgs" /> object that contains the event data.</param>
    protected override void OnDataBinding(EventArgs e)
    {
      _databound = true;
      base.OnDataBinding(e);
    }

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init" /> event.
    /// </summary>
    /// <param name="e">An <see cref="T:System.EventArgs" /> object that contains the event data.</param>
    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);

      if (!DatabindOnLoad || Model == null)
        return;

      try
      {
        DataBind();
      }
      catch (Exception exception)
      {
        Log.Error(String.Format("Error while initializing model bound user control of type '{0}'", typeof(T).FullName), exception, this);
        throw;
      }
    }

    /// <summary>
    /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter" /> object, which writes the content to be rendered on the client.
    /// </summary>
    /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter" /> object that receives the server control content.</param>
    protected override void Render(HtmlTextWriter writer)
    {
      if (SourceItem == null && Model == null)
      {
        Controls.Clear();

        if (!Sitecore.Context.PageMode.IsNormal && ShowDataSourceUnavailableInfoText)
          RenderDatasourceUnavailableInfo(writer);

        return;
      }

      if (SourceItem != null && !SourceItem.IsPublishable())
      {
        Controls.Clear();

        if (!Sitecore.Context.PageMode.IsNormal && ShowDataSourceUnpublishableInfoText)
          RenderDatasourceUnpublishableInfo(writer);

        return;
      }

      if (Model == null)
      {
        Controls.Clear();
        return;
      }

      if (_databound == false)
      {
        Controls.Clear();
        return;
      }
      base.Render(writer);
    }

    /// <summary>
    /// Renders the datasource unpublishable information if the datasource used by the Model instance is unpublishable.
    /// </summary>
    /// <param name="writer">The writer.</param>
    private void RenderDatasourceUnpublishableInfo(HtmlTextWriter writer)
    {
      Control control = new DataSourceInfo(DataSourceUnpublishableInfoText);
      control.RenderControl(writer);

      if (SourceItem != null && Sitecore.Context.Item != null)
      {
        Log.Error(String.Format("Datasource is unpublishable for source item '{0}' on item '{1}'", SourceItem.Paths.FullPath, Sitecore.Context.Item.Paths.FullPath), this);
      }
    }

    /// <summary>
    /// Renders the datasource unavailable information if the datasource used by the Model instance is unavailable.
    /// </summary>
    /// <param name="writer">The writer.</param>
    private void RenderDatasourceUnavailableInfo(HtmlTextWriter writer)
    {
      Control control = new DataSourceInfo(DataSourceUnavailableInfoText);
      control.RenderControl(writer);

      if (SourceItem != null && Sitecore.Context.Item != null)
      {
        Log.Error(String.Format("Datasource is unavailable for source item '{0}' on item '{1}'", SourceItem.Paths.FullPath, Sitecore.Context.Item.Paths.FullPath), this);
      }
    }

    private Item _sourceItem;
    private IModelFactory _modelFactory;
    private Boolean _databound;
  }
}

The only thing to note is that this is not the way you should do it (unless if it is a PoC) and that in some rare cases multiple inheritance really would have been beneficial.

Anyway with that out of the way the only thing that is missing is to create our actual user controls for the NewsList and the News presentation.

The only things to note about the user controls are that they both inherit from GlassModelBoundUserControl and specify the type of the model class and that they contain absolutely no code in the codebehinds – just like all good codebehinds should.

The NewsView user control

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="NewsView.ascx.cs" Inherits="PT.GlassItemBindingExamples.Presentation.NewsView" %>

<div class="news">
  <h2><%=Editable(x => x.Title) %></h2>
  <div class="paragraph"><%=Editable(x => x.Text) %></div>
</div>
using PT.Framework.ItemBinding.Glass.Presentation;
using PT.GlassItemBindingExamples.Model;

namespace PT.GlassItemBindingExamples.Presentation
{
  public partial class NewsView : GlassModelBoundUserControl<GlassNews>
  {
  }
}

The NewsView user control

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="NewsListView.ascx.cs" Inherits="PT.GlassItemBindingExamples.Presentation.NewsListView" %>

<div class="news-list">
  <h2><%=Editable(x => x.Title) %></h2>

  <asp:Repeater DataSource="<%# Model.News.OrderByDescending(news => news.Created) %>" ItemType="PT.GlassItemBindingExamples.Model.GlassNews" runat="server">
    <HeaderTemplate>
      <div class="news">
    </HeaderTemplate>  
    <ItemTemplate>
      <div class="news-item">
        <div class="date"><%# Editable(Item, x => x.Created, x => x.Created.ToString("dd. MMM yyyy")) %></div>
        <h3><a href="<%# Item.Url %>"><%# Editable(Item, x => x.Title) %></a></h3>
        <div class="teaser"><%# Editable(Item, x => x.Teaser) %></div>
      </div>
    </ItemTemplate>
    <FooterTemplate>
    </div>
    </FooterTemplate>
  </asp:Repeater>
</div>
using PT.Framework.ItemBinding.Glass.Presentation;
using PT.GlassItemBindingExamples.Model;

namespace PT.GlassItemBindingExamples.Presentation
{
  public partial class NewsListView : GlassModelBoundUserControl<GlassNewsList>
  {
  }
}

Well that pretty much wraps it up. I do hope that some of you found this post informative. The purpose of course was not to create something that was fully fledged production ready but to present a PoC of how the Sitecore ItemBinding framework can be used with pretty much any kind, form, shape and type of model classes that meets your individual requirements.

As always the source code and binaries for the Sitecore ItemBinding framework are on GitHub https://github.com/BoBreiting/SitecoreItemBinding, Sitecore Marketplace https://marketplace.sitecore.net/en/Modules/Sitecore_ItemBinding.aspx or directly from NuGet – search for ItemBinding.


Viewing all articles
Browse latest Browse all 43

Trending Articles