Josef Ottosson's Picture

Josef Ottosson

7 posts

Customizing PropertyHandlers in JOS.ContentSerializer

The problem

Imagine the following ContentType:

public class PostPage : PageData  
{
    ....
    [Display(Order = 400)]
    [CultureSpecific]
    [AllowedTypes(typeof(AuthorBlock))]
    public virtual ContentReference Author { get; set; }
    ...
}

As you can see, it's only possible to select AuthorBlocks in the Author-property.
The default implementation of ContentReferencePropertyHandler looks like this:

public class ContentReferencePropertyHandler : IPropertyHandler<ContentReference>  
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;

    public ContentReferencePropertyHandler(IUrlHelper urlHelper, IContentSerializerSettings contentSerializerSettings)
    {
        _urlHelper = urlHelper;
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
    }

    public object Handle(ContentReference contentReference, PropertyInfo propertyInfo, IContentData contentData)
    {
        if (contentReference == null || contentReference == ContentReference.EmptyReference)
        {
            return null;
        }

        var url = new Uri(this._urlHelper.ContentUrl(contentReference, this._contentSerializerSettings.UrlSettings));

        if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls && url.IsAbsoluteUri)
        {
            return url.AbsoluteUri;
        }

        return url.PathAndQuery;
    }
}

What will happen when we run .ToJson?
Well, since it's impossible to link directly to a block, the output will look like this:

{
    ...
    "author": "http://localhost:54321/"
    ...
}

That's not nice, it would be better if we could display the actual block instead.

The solution

Simply create a new class implementing the IPropertyHandler<ContentReference> interface and then register it in the DI container.

Improved ContentReferenceHandler

public class CustomContentReferencePropertyHandler : IPropertyHandler<ContentReference>  
{
    private readonly IUrlHelper _urlHelper;
    private readonly IContentSerializerSettings _contentSerializerSettings;
    private readonly IContentLoader _contentLoader;
    private readonly IPropertyHandler<BlockData> _blockDataPropertyHandler;

    public CustomContentReferencePropertyHandler(
        IUrlHelper urlHelper,
        IContentSerializerSettings contentSerializerSettings,
        IContentLoader contentLoader,
        IPropertyHandler<BlockData> blockDataPropertyHandler)
    {
        _urlHelper = urlHelper ?? throw new ArgumentNullException(nameof(urlHelper));
        _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
        _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        _blockDataPropertyHandler = blockDataPropertyHandler ?? throw new ArgumentNullException(nameof(blockDataPropertyHandler));
    }

    public object Handle(ContentReference contentReference, PropertyInfo property, IContentData contentData)
    {
        if (contentReference == null || contentReference == ContentReference.EmptyReference)
        {
            return null;
        }

        if (IsReferenceToBlock(property))
        {
            if (this._contentLoader.TryGet<BlockData>(contentReference, out var blockData))
            {
                return this._blockDataPropertyHandler.Handle(blockData, property, contentData);
            }
        }

        var url = new Uri(this._urlHelper.ContentUrl(contentReference, this._contentSerializerSettings.UrlSettings));

        if (this._contentSerializerSettings.UrlSettings.UseAbsoluteUrls && url.IsAbsoluteUri)
        {
            return url.AbsoluteUri;
        }

        return url.PathAndQuery;
    }

    private static bool IsReferenceToBlock(MemberInfo property)
    {
        var allowedTypesAttribute = property.GetCustomAttribute<AllowedTypesAttribute>();

        if (allowedTypesAttribute?.AllowedTypes == null || !allowedTypesAttribute.AllowedTypes.Any())
        {
            return false;
        }

        return allowedTypesAttribute.AllowedTypes.All(x => typeof(BlockData).IsAssignableFrom(x));
    }
}

Replace the default implementation

Replace for all properties of type ContentReference

[InitializableModule]
[ModuleDependency(typeof(JOS.ContentSerializer.Internal.ContentSerializerInitalizationModule))]
public class ContentSerializerInitializationModule : IConfigurableModule  
{
    public void Initialize(InitializationEngine context) {}

    public void Uninitialize(InitializationEngine context) {}

    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Services.RemoveAll<IPropertyHandler<ContentReference>>();
        context.Services.AddSingleton<IPropertyHandler<ContentReference>, CustomContentReferencePropertyHandler>();
    }
}

Replace for specific property

[Display(Order = 400)]
[CultureSpecific]
[AllowedTypes(typeof(AuthorBlock))]
[ContentSerializerPropertyHandler(typeof(CustomContentReferencePropertyHandler))]
public virtual ContentReference Author { get; set; }  

JOS.ContentSerializer 3.0 has been released!

New features

Support for more built in properties

The following properties are now supported out of the box:

  • BlockData
  • bool
  • double
  • ContentArea
  • ContentReference
  • DateTime
  • IEnumerable<ContentReference>
  • int
  • LinkItemCollection
  • PageReference
  • PageType
  • string[]
  • string SelectOne/SelectMany support.
  • Url
  • XhtmlString
  • NEW IEnumerable<string> ICollection/IList works as well
  • NEW IEnumerable<int> ICollection/IList works as well
  • NEW IEnumerable<double> ICollection/IList works as well
  • NEW IEnumerable<DateTime> ICollection/IList works as well

Easier to add support for custom properties

The old solution to add support for custom properties sucked. I've rebuilt it from scratch.

Example:
You're using the property Jos.PropertyKeyValueList on your StartPage like this.

public class StartPage : PageData  
{
    ...
    public virtual string Heading { get; set; }
    public virtual IEnumerable<KeyValueItem> KeyValueItems{ get; set; }
    ...
}

Now, if you call .ToJson on a StartPage instance you would only get the Heading property in the json output since IEnumerable<KeyValueItem> isn't handled out of the box.

{
    "heading" : "Where is my KeyValueItems??"
}

To add support for it, first create a new class that implements the IPropertyHandler<> interface

public class KeyValueItemListPropertyHandler : IPropertyHandler<IEnumerable<KeyValueItem>>  
{
    public object Handle(IEnumerable<KeyValueItem> value, PropertyInfo property, IContentData contentData)
    {
        // Do whatever you want with the property here.
        return value;
    }
}

Then register your class in your DI container.
Example

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class MyConfigurationModule : IConfigurableModule  
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    { 
    context.Services.AddSingleton<IPropertyHandler<IEnumerable<KeyValueItem>>, KeyValueItemListPropertyHandler>();
    }
}

Now, if you would call .ToJson again on your StartPage instance, you would see the following output.

{
    "heading": "Where is my KeyValueItems??",
    "keyValueItems": [
        {
            "key": "Some key",
            "value": "Hello there"
        },
        {
            "key": "Another key",
            "value": "Another value!"
        }
    ]
}

Easier to extend/replace built in PropertyHandlers

Say that you, for some reason, want all strings to return "JOSEF OTTOSSON!!" instead of their actual value.

Just create a new propertyhandler for strings like this.

public class JosefStringPropertyHandler : IPropertyHandler<string>  
{
    public object Handle(string value, PropertyInfo property, IContentData contentData)
    {
        return "JOSEF OTTOSSON!!";
    }
}

Then swap out the default StringPropertyHandler in the DI container like this:

context.Services.AddSingleton<IPropertyHandler<string>, JosefStringPropertyHandler>();  

Custom PropertyHandler for specific property

It's possible to specify which PropertyHandler to use by adding the ContentSerializerPropertyHandlerAttribute on the desired property.
NOTE, this is currently an experimental feature, it works, but I've had no time to write tests yet, so consider it a BETA until further notice ;)

Added a __type__ property to contentarea items

I've added a property to all contentarea items named __type__.
It will be added automatically to the json output. It looks like this:

"mainContentArea": {
    "jumbotronBlock": [{
        ....
        "heading": "Wherever you meet!",
        "__type__": "JumbotronBlock"
        ....
    }],
    "teaserBlock": [{
    ....
    "heading": "Alloy Plan",
    "__type__": "TeaserBlock"
    ....
    }]
},

It's possible to change the property name by setting the BlockTypePropertyName(just realised that that name sucks, will fix it in the next release) property in the IContentSerializerSettings.

Breaking changes

  • All PropertyHandler interfaces are gone(IStringPropertyHandler, IStringArrayPropertyHandler and so on). Use IPropertyHandler<> instead.
  • ContentReferenceSettings has been removed, use IUrlSettings instead.
  • Added MIT License.
  • ContentAreaPropertyHandler now uses .FilteredItems instead of .Items.

As always, all code can be found on Github