episerver mvc

2 posts

How to create a custom EPiServer 7 property with Dojo

I've used a lot of custom EPiServer properties at work but I've never built my own. Luckily for me, one client at work gave me a new project to work on. It's somewhat like a blog page, he wants to display different articles on a page. The articles should be splitted up using Tags.

I was afraid at first, how do I build that? Sure, I could just let him enter the tags in a ordinary string field and separate the tags with a comma. But that is not a good solution at all.

Let's play with Dojo!

One note before I begin, I found this project(GETA.Tags) and I've looked at that project for inspiration/how I should structure the project and stuff like that...

Enough talk, time for some CODE!

The first thing you wanna do when creating a custom property is to create a EditorDescriptor. It looks like this:
JoTagsEditorDescriptor.cs

using EPiServer.Shell.ObjectEditing.EditorDescriptors;  
namespace PlayTime.Business.Properties.JoTags  
{
    [EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "JoTagsProperty")]
    public class JoTagsEditorDescriptor : EditorDescriptor
    {
        public JoTagsEditorDescriptor()
        {
            ClientEditingClass = "app.editors.JoTagsProperty";
        }
    }
}

Then you will create the actuall "Property class". Here you could do some custom validation and stuff like that when saving/retrieving the data.
PropertyJoTags.cs

using System;  
using EPiServer.Core;  
using EPiServer.PlugIn;

namespace PlayTime.Business.Properties.JoTags  
{
[PropertyDefinitionTypePlugIn(Description = "A property for tagging content", DisplayName = "Jo Tags")]
    public class PropertyJoTags : PropertyString
    {
        public override Type PropertyValueType
        {
            get { return typeof(string); }
        }

        public override object Value
        {
            get
            {
                var value = base.Value as string;
                if (value == null)
                {
                    return string.Empty;
                }

                return value;
            }
            set
            {
                if (value == null)
                {
                    value = string.Empty;
                }

                base.Value = value;

            }
        }

        public override object SaveData(PropertyDataCollection properties)
        {
            return String;
        }
    }
}

That's pretty much the EPiServer side done. You will need to register your property in a file named module.config in your project root, if the file doesn't exists, create it. In that file you will register your property as well as your propertys dependencies(in my case, jQuery, jQuery UI and TagIt)

My file looks like this:
module.config

<module>  
    <assemblies>
        <add assembly="PlayTime" />
    </assemblies>

    <dojoModules>
        <add name="app" path="Scripts" />
        <add name="JoTags" path="JoTags" />
    </dojoModules>

    <clientResources>
        <add name="JoTagsVendor" resourceType="Script" sortIndex="1" path="JoTags/Vendor/jquery-1.11.2.min.js" />
        <add name="JoTagsVendor" resourceType="Script" sortIndex="2" path="JoTags/Vendor/jquery-ui.min.js" />
        <add name="JoTagsVendor" resourceType="Script" sortIndex="3" path="JoTags/Vendor/tag-it.min.js" />
       <add name="JoTagsVendor" resourceType="Style" sortIndex="4" path="JoTags/Vendor/jquery-ui.min.css" />
       <add name="JoTagsVendor" resourceType="Style" sortIndex="5" path="JoTags/Vendor/jquery.tagit.css" />
       <add name="JoTagsVendor" resourceType="Style" sortIndex="6" path="JoTags/Vendor/tagit.ui-zendesk.css" />
      </clientResources>

      <clientModule>
          <moduleDependencies>
              <add dependency="CMS" type="RunAfter" />
          </moduleDependencies>
          <requiredResources>
              <add name="JoTagsVendor" />
              <add name="JoTags" />
          </requiredResources>
      </clientModule>
</module>  

The "add assembly" part is the name of your project, in my case, "PlayTime". Then you will register your Dojo module and set a correct path. The path is relative to the ClientResources folder.
If that folder doesn't exists in your project root, create it.
Here's an image of my folder structure:

The javascript(Dojo code) is located in the Scripts/Editors folder. It's named JoTagsProperty.js. I placed it there because all of my other custom properties had their editor files placed there, you can place your wherever you want as long as the path(ClientEditingClass) in the EditorDescriptor class is correct :)

Dojo time!

JoTagsProperty.js

define([  
    "dojo/_base/declare",
    "dojo/_base/array",
    "dijit/form/TextBox"
 ],
 function (
    declare,
    array,
    TextBox) {
    return declare([TextBox], {
        postCreate: function () {
            var that = this;
            var input = $(this.domNode).find('input[name="tags"]');
            $.getJSON("/jotags", function (data) {
                that._setUpTagIt(input, data);
            });
    },

    _setUpTagIt: function (input, tags) {
        var that = this;
        $(input).tagit({
            availableTags: tags,
            allowSpaces: true,
            removeConfirmation: true,
            beforeTagAdded: function (evt, ui) {
                if (ui.duringInitialization) return;

                var userInputedTag = ui.tagLabel.toLowerCase();
                var tagAllowed = that._isTagAllowed(userInputedTag, tags);

                if (!tagAllowed) {
                    return false;
                }
            }
        });
    },

    _isTagAllowed: function (wantedTag, tags) {
        var exists = array.indexOf(tags, wantedTag);
        if (exists > -1) {
            return true;
        }
        return false;
    }
    });
});

You can read more about what methods Dojo uses here.

The postCreate function runs when the widget has been rendered and you can also access the DOM node here.

My property works like this:

  1. Get the input DOM element
  2. Fetch the available tags from my backend
  3. Initialize TagIt on the input field
  4. Profit!

The isTagAllowed function checks if the user has entered a tag that's allowed, if not, it will be removed.

EPiServer ContentData to JSON

pug with puppies

Note, the project has seen a major update(even more awesomeness), you can read more at GitHub or here

When working with EPiServer you would normally use standard Razor views to render your frontend code...you will get syntax support in Visual Studio and can use many nice little HtmlHelpers, I love it. But...im a backend developer. The frontend people at my work hates Visual Studio. They are also using some cool sounding tools like Backbone and Handlebars so they don't want to use ordinary Razor views...

In another blog post I showed how we transfered the EPiServer properties to our frontend templates so Im not going to show that again, but basically we are dumping a javascript object in the DOM and then our frontend framework picks it up and generates the markup.

So...how are we generating the JSON?

First I built a thing that took all items in a ContentArea, filtered out all items that had a specific attribute on their Class and inherited from a specific Class, then I selected all properties in the class that had a specific attribute. It worked but it was very annoying to set it up to say the least. One more huge annoyance was that it didn't supported ContentAreas and nested Internalblocks.

So this weekend I rebuilt everything. My goal was to get rid of the inheritance/attribute-mayhem, support nested blocks/contentAreas(recursive) and make it easy for developers to use it.

I think I've succeeded :)

Currently the following property types are supported:

  • String
  • bool
  • XhtmlString
  • ContentArea (different ContentTypes as well!)
  • InternalBlock (using Blocks as a property)
  • Double
  • Int
  • DateTime
  • SelectOne
  • SelectMany
  • PageReference
  • ContentReference
  • LinkItemCollection
  • Url

Let me show you guys how it can be used:

public class StartpageController : PageController<Startpage>  
{
    public string Index(Startpage currentPage)
    {
        Response.ContentType = "application/json";
        var json = currentPage.ToJson();
        return json;
    }
}

That's it! I've created an extension method to the ContentData-type so it works both on pages and blocks.

This is how my Startpage looks:

[ContentType(DisplayName = "Startpage", GUID = "a6762bfb-973b-41c1-acf8-7d26567cd71d", Description = "")]
    public class Startpage : PageData
    {
        [Display(Name = "String", Order = 100)]
        [JsonProperty("string")]
        public virtual string Heading { get; set; }

        [Display(Name = "XhtmlString", Order = 110)]
        [JsonProperty("xhtmlString")]
        public virtual XhtmlString HtmlText { get; set; }

        [Display(Name = "InternalBlock", Order = 160)]
        [JsonProperty("internalBlock")]
        public virtual InternalBlock InternalBlock { get; set; }

        [Display(Name = "Contentarea", Order = 170)]
        [JsonProperty("contentArea")]
        public virtual ContentArea ContentArea { get; set; }
    }

The only thing that needs to be added to make the property appear in the JSON output is the JsonProperty-attribute(Newtonsoft).

Im thinking about removing that and instead use the Display-attribute and make the JsonProperty-attribute optional if you want to specify a custom JSON key.

Note, the above is now implemented, you dont need the JsonProperty attribute. Read more at GitHub. EPiServer - ContentJson

The JSON genereated would look like this:

{
        "heading": "This is the heading",
        "body": "<p>This is cool because <strong>JOSEF WROTE IT</strong></p>",
        "internalBlock": {
            "heading" : "Internalblock Heading here",
            "subHeading" : "This is my subheading"
        },
        "contentArea": {
            "sharedBlockName" : [
                {
                    "heading" : "Hello",
                    "subHeading" : "Hey you"
                },
                {
                    "heading" : "Goodbye",
                    "subHeading" : "Cheers"
                }
            ],
            "rappersBlock" : [
                {
                    "name" : "Eminem",
                    "isTheGreatestRapperEver": true,
                    "sucks" : false
                },
                {
                    "name" : "Big Sean",
                    "isTheGreatestRapperEver" : false,
                    "sucks" : false
                },
                {
                    "name" : "Flo Rida",
                    "isTheGreatestRapperEver" : false,
                    "sucks" : true
                }
            ]
        }
    }

That's just a simple example, head over to the GitHub repo and look at some other examples and try it out!

Don't forget to look at how I solved the SelectOne/SelectMany property type, I think it turned out quite nicely

XForms support

Yeah...im thinking about it, but I will not implement it right now, it will come, someday. Right now I just don't feel like parsing XML... :)