I am working on a project that uses code-generated interfaces passed as models into views.
I came with a nice way of validating these models being passed, implementing C# feature called pattern matching. Now validation works for me by just dropping the below snippet onto my view:
@if (Html.Validate<ICompositeTemplate>(Model) is var e && e != null)
{
@e { return; }
}
What he above code doing is validating that:
- model was passed and is not null
- the model is of a given template
- when the datasource template uses composition from numerous interface templates, the passed interface is also implementing those numerous code-generated interfaces for each of template using for composition. I an checking all them being actually composed and the model being mapped and passed using the right template
- when validation fails - show users meaningful error messages, but do that only for editors using Experience Editor
For example, the above definition of ICompositeTemplate
could be looking as below:
public interface ICompositeTemplate : ITitleWithRichText, ICtaWithSvg
{
}
where ICtaWithSvg
is composite on its own:
public interface ICtaWithSvg : ICta, ISvg
{
}
Both implemented interfaces inherit from IGlassBase
and also have SitecoreType
attribute:
[SitecoreType(TemplateId = ITitleWithRichTextConstants.TemplateIdString)]
[GeneratedCode("Leprechaun", "2.0.0.0")]
public partial interface ITitleWithRichText : IGlassBase
{
// code-generated implementation
}
Looking at Sitecore, the actual template is implemented at the Project layer from a composition of the interface templates:
That actually works like a charm, exactly as I want it to perform!
Show me the code! The code of HTML Helper that makes all that function:
using System.Web.Mvc;
using Sitecore;
using System;
using Sitecore.Data;
using Sitecore.Data.Items;
using Foundation.Data.Models;
using System.Linq;
using Glass.Mapper.Sc.Configuration.Attributes;
using System.Reflection;
using System.Collections.Generic;
namespace Feature.Components.Extensions
{
public static class ModelValidationExtensions
{
public static MvcHtmlString Validate<T>(this HtmlHelper html, T model) where T : IGlassBase
{
if (model == null)
return new MvcHtmlString("Datasource is missing for this component");
var featureInterfaceTemplateIds = GetTemplateId<T>();
var modelItem = Context.Database.GetItem(new ID(model.Id));
var failedTemplates = new List<Guid>();
foreach (var templateId in featureInterfaceTemplateIds)
{
var section = modelItem?.GetAncestorOrSelfOfTemplate(new ID(templateId));
if (section == null)
{
failedTemplates.Add(templateId);
}
}
if (failedTemplates.Any())
{
var ids = failedTemplates.Select(ft => ft.ToString());
var error = String.Empty;
if (Context.PageMode.IsExperienceEditor)
{
error = $@"<div class='EE_error'>
<div>Provided datasource has invalid type(s).</div>
<div>Please correct it to inherit: {string.Join(", ", $"{{{ids}}}")}.</div>
</div>";
}
return new MvcHtmlString(error);
}
return null;
}
private static Item GetAncestorOrSelfOfTemplate(this Item item, ID templateID)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
return item.DescendsFrom(templateID)
? item
: item.Axes.GetAncestors().LastOrDefault(i => i.DescendsFrom(templateID));
}
private static bool SitecoreTypeAttributeFilter(Type referencedType, Object criteriaObj)
{
var attribs = referencedType.CustomAttributes;
var customAttributes = attribs.FirstOrDefault(a => a.AttributeType == typeof(SitecoreTypeAttribute));
if (customAttributes != null)
{
var tmplId = customAttributes.NamedArguments.FirstOrDefault(a => a.MemberName == "TemplateId");
if (tmplId != null)
{
return true;
}
}
return false;
}
private static IEnumerable<Guid> GetTemplateId<T>() where T : IGlassBase
{
var guids = new List<Guid>();
var referencedType = typeof(T);
var filter = new TypeFilter(SitecoreTypeAttributeFilter);
var _ifs = referencedType.FindInterfaces(filter, "IGlassBase").ToList();
_ifs.Add(referencedType);
foreach (var type in _ifs)
{
var attribs = type.CustomAttributes;
var customAttributes = attribs
.FirstOrDefault(a => a.AttributeType == typeof(SitecoreTypeAttribute));
if (customAttributes != null)
{
var tmplId = customAttributes.NamedArguments
.FirstOrDefault(a => a.MemberName == "TemplateId");
if (tmplId != null)
{
guids.Add(Guid.Parse(tmplId.TypedValue.ToString().Trim('\"')));
}
}
}
return guids;
}
}
}
It allows me saving lots of time without losing in quality and keeping the models validating. If something goes wrong - both me and editors know what exactly need to get fixed.
Hope you benefit from validating your MVC models too!