Creating custom page properties in EPiServer has undergone some changes in the latest version of the CMS. No more ASP.NET server side controls. Instead you are required to write a client side representation of your property in Dojo. A choice criticized by many, but once you’ve gotten the hang of the Dojo way of doing things it actually becomes a quite reasonable choice. Dojo is a widely used and very well documented toolkit that excels in widget-like functionality and offers a very wide arrange of different base classes to inherit functionality and properties from. The documentation is excellent (unfortunately the EPiServer documentation is extremely lacking) and as long as you have a basic understanding of JavaScript creating simple plugins is trivial.

The first thing you need to understand is the anatomy of a Dojo plugin, so lets have a look.

define([

    // These are the dependencies for our created widget.
    'dojo/dom',
   '_templatedMixin'

], function(dom, _templatedMixin){

    // Once all modules in the dependency list have loaded, this
    // function is called. It's important to understand that this function need to be 
    // passed all the modules in your dependency list, for example we have the 'dojo/dom' dependency
    // which is why we pass our function an argument of dom.
    // This is so that we can access those dependencies in our actual widget.

    declare('namespace.ourawesomeproperty', _templatedMixin, {

         // The next step is to call the declare function
         // The first argument is the name and optional namespace of our newly created widget
         // The second argument is the widget we inherit from.
         // If we want to inherit several widgets we can put them in an array []
         // The third argument is this javascript object (the one that contains this comment).
         // This is where we declare any custom properties or functions for our widget

        myprop: 'Dojo is awesome!'
    });
});

It looks more daunting than it is. Dijit widgets are defined in the AMD (Asynchronous Module Definition) pattern where you first define all the dependencies of your module, then declare your actual JavaScript object (with the declare function).

But once we have a Dijit module defined and ready to go we need to somehow hook it up to EPiServer and tell EPi which property we want to use our shiny module for. This isn’t very difficult either. We simply decorate any property on a ContentType with the ClientEditor attribute, like this:

[ClientEditor(ClientEditingClass = "ournamespace.ourawesomewidget")]
public virtual string SomeProperty { get; set; }

And suddenly all strings will be handled by the ournamespace.ourawesomewidget module. Pretty neat huh?

But hey, wait a minute, where do we actually put our created Dijit module for EPiServer to find it? This is an excellent question and here we unfortunately need to mix in some site configuration. In the module.config you can define where the Dojo loader should look for files with a specific namespace. In the alloy templates site we find this:

<dojo>
    <!-- Add a mapping from alloy to ~/ClientResources/Scripts 
    to the dojo loader configuration -->
    <paths>
        <add name="alloy" path="Scripts" />
    </paths>
</dojo>

Ok, so there’s a <dojo> tag where we can add <paths>, apparently this automatically maps to the ~/ClientResources/ root folder. Also worth noting is that the name of our added path (in this case alloy) must correspond exactly to the namespace of our modules for Dojo to understand where to look for them, this means that if your namespace alloy.somenamespace.widgetname with a modules config as the one above the path to your widget must be ~/ClientResources/Scripts/somenamespace/widgetname.js.

I hope all that made sense…

Anyway, enough with the background already, lets actually build something. Imagine our client wants to add some sort of property editor for creating polls to their site. Each poll has a title and a couple of possible answers.

Allright, lets start with modeling the data real quick. I’m gonna use ASP.NET Web Api as the back end for our polls.

public class PollsController : ApiController
{
    public List<SimplePoll> data = new List<SimplePoll>() {

        new SimplePoll {
             Header = "Who's your favorite chess player?",
             PollId = 1,
             Answers = new List<SimpleAnswer> {
                        new SimpleAnswer() {
                            Answer = "Bobby Fischer",
                            AnswerId = 1
                        },
                        new SimpleAnswer() {
                            Answer = "Magnus Carlsen",
                            AnswerId = 2
                        },
                        new SimpleAnswer() {
                            Answer = "José Raul Capablanca",
                            AnswerId = 3
                        },
                        new SimpleAnswer() {
                            Answer = "Garry Kasparov",
                            AnswerId = 4
                        },
                        new SimpleAnswer() {
                            Answer = "Emanuel Lasker",
                            AnswerId = 5
                        },
                    }
                }, 
        new SimplePoll {
             Header = "What's your favorite chess opening?",
             PollId= 2,
             Answers = new List<SimpleAnswer> {
                        new SimpleAnswer() {
                            Answer = "Sicilian dragon",
                            AnswerId = 1
                        },
                        new SimpleAnswer() {
                            Answer = "Scottish",
                            AnswerId = 2
                        },
                        new SimpleAnswer() {
                            Answer = "Queens Gambit",
                            AnswerId = 3
                        },
                        new SimpleAnswer() {
                            Answer = "English",
                            AnswerId = 4
                        },
                        new SimpleAnswer() {
                            Answer = "Kings gambit",
                            AnswerId = 5
                        },
                    }
                }
        };

    // GET api/
    public IEnumerable<SimplePoll> Get()
    {
        return data;
    }

    public SimplePoll Get(int id)
    {
        return data.FirstOrDefault(p => p.PollId == id);
    }

    public void Delete(int id)
    {
        data.Remove(data.FirstOrDefault(p => p.PollId == id));
    }
}

public class SimplePoll
{
    public int PollId { get; set; }
    public string Header { get; set; }
    public List<SimpleAnswer> Answers { get; set; }
}

public class SimpleAnswer
{
    public int AnswerId { get; set; }
    public string Answer { get; set; }
}

Just a real simple API controller with an in memory representation of our data. We’re going to need an answers controller as well...

public class AnswersController : ApiController
    {
        public void Delete(int id)
        {
            //Nah, don't wanna delete it
        }
    }

Ok, well that would of course work a little bit different if we actually had some sort of database or something to persist our polls and answers to, but we don’t, so lets just have a look at the Dijit now…

We start by configuring up a new namespace in the module.config (we could of course use the already existing alloy namespace, but where’s the fun in that?). Note how we can have two namespaces pointing to the same path without a problem.

<dojo>
        <!-- Add a mapping from alloy to ~/ClientResources/Scripts to the dojo loader configuration -->
        <paths>
            <add name="alloy" path="Scripts" />
            <add name="whatever" path="Scripts" />
        </paths>
    </dojo>

I create a Dijit module PollCreation.js:

define([
    "dojo/_base/connect",
    "dojo/_base/declare",
    "dojo/store/Memory",
    "dijit/_CssStateMixin",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetsInTemplateMixin",
    "dijit/form/FilteringSelect",
    "dijit/TitlePane",
    "epi/dependency",
    "epi/epi",
    "epi/shell/widget/_ValueRequiredMixin",
    "epi/shell/widget/dialog/Dialog",
    "epi/cms/widget/_HasChildDialogMixin",
    "whatever/editors/PollDialog",
    "whatever/editors/DeleteableTextBox",
    "dojo/text!./templates/poll.html"
],
function (
    connect,
    declare,
    Memory,
    _CssStateMixin,
    _Widget,
    _TemplatedMixin,
    _WidgetsInTemplateMixin,
    FilteringSelect,
    TitlePane,
    dependency,
    epi,
    _ValueRequiredMixin,
    Dialog,
    _HasChildDialogMixin,
    PollDialog,
    DeleteableTextBox,
    template
) {
    return declare("whatever.editors.PollCreation", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin, _HasChildDialogMixin], {
        templateString: template,
        intermediateChanges: false,
        value: null,
        store: null,
        currentPollData: null,
        postCreate: function () {
            // call base implementation
            this.inherited(arguments);

            this.connect(this.selectPolls, "onChange", this._onInputWidgetChanged);

            this._refreshDropDown(this.get('value'));
            this._refreshAnswers(this.get('value'));
        },
        isValid: function () {
            return this.selectPolls.isValid();
        },
        // Setter for value property
        _setValueAttr: function (value) {
            this.selectPolls.set("value", value);
            this._set("value", value);
        },
        _setReadOnlyAttr: function (value) {
            this._set("readOnly", value);
            this.selectPolls.set("readOnly", value);
        },
        _updateValue: function (value) {
            if (this._started && epi.areEqual(this.value, value)) {
                return;
            }

            this._set("value", value);
            this._refreshAnswers(value);
            this.onChange(value);
        },
        _createDialog: function () {
            this.pollDialog = new PollDialog({

            });

            this.dialog = new Dialog({
                title: "New poll",
                content: this.pollDialog,
                dialogClass: "epi-dialog-portrait"
            });

            this.connect(this.dialog, 'onExecute', '_onExecute');
            this.connect(this.dialog, 'onHide', '_onDialogHide');
            this.dialog.startup();
        },
        _onDialogHide: function () {
            this.focus();
        },
        _onButtonClick: function () {
            if (!this.dialog) {
                this._createDialog();
            }

            this.dialog.show(true);
        },
        _onExecute: function () {           
            var pollData = this.pollDialog.get('value');

            var poll;

            jQuery.ajax({
                url: '/api/polls?pollHeading=' + pollData.pollHeading + '&answers='+pollData.answers,
                type: 'POST',
                contentType: 'application/json; charset=utf-8',
                success: function (result) {
                    poll = JSON.parse(result);
                },
                async: false
            });

            this._refreshDropDown(poll.PollId);
            this.selectPolls.refresh();
        },
        _refreshDropDown: function (selected) {
            var pollData = [];

            jQuery.ajax({
                url: '/api/polls',
                contentType: 'application/json; charset=utf-8',
                success: function (result) {
                    result = JSON.parse(result);

                    //push empty result in case they do not want a poll
                    pollData.push({ name: '', id: -1 });

                    for (var i = 0; i < result.length; i++) {
                        pollData.push({ name: result[i].Header, id: result[i].PollId });
                    }
                },
                async: false
            });

            var store = new Memory({
                data: pollData
            });

            this.selectPolls.store = store;
            this.selectPolls.set('value', selected);
        },
        _refreshAnswers: function (selected) {
            this.titlePane.destroyDescendants();

            if (selected <= 0)
                return;

            var selectedPoll;

            jQuery.ajax({
                url: '/api/polls/'+selected,
                contentType: 'application/json; charset=utf-8',
                success: function (result) {

                    selectedPoll = JSON.parse(result);

                },
                async: false
            });

            for (var i = 0; i < selectedPoll.Answers.length; i++) {
                var textBox = new DeleteableTextBox({
                    name: 'text',
                    readOnly: true,
                    value: selectedPoll.Answers[i].Answer,
                    style: 'display:table; margin-bottom:10px;',
                    answerId: selectedPoll.Answers[i].AnswerId
            });
                this.titlePane.addChild(textBox, i);
            }
        },
        _onDeletePollClick: function() {
            if (confirm('Are you sure you want to delete this magnificent poll?')) {
                jQuery.ajax({
                    url: '/api/polls/' + this.get('value'),
                    type: 'DELETE',
                    contentType: 'application/json; charset=utf-8',
                    success: function (result) {
                    },
                    async: false
                });

                this._refreshAnswers(-1);
                this._refreshDropDown(-1);
            }
        },
        _onInputWidgetChanged: function(value) {
             this._updateValue(value);
        }
    });
});

Allright, now that’s quite a mouthful of javascript. Lets go through that top to bottom. First we declare our dependencies:

define([
    //These are some basic dojo-modules we need
    "dojo/_base/connect",
    "dojo/_base/declare",
    "dojo/store/Memory",

    //Some dijits that we want to utilize and inherit
    "dijit/_CssStateMixin",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetsInTemplateMixin",
    "dijit/form/FilteringSelect",
    "dijit/TitlePane",

    //Oh look, epi has quite a few modules that we can use as well.
    //Unfortunately there's close to zero documentation, but hopefully we'll get some in the future.
    //Anyway, these are some modules that we need...
    "epi/dependency",
    "epi/epi",
    "epi/shell/widget/_ValueRequiredMixin",
    "epi/shell/widget/dialog/Dialog",
    "epi/cms/widget/_HasChildDialogMixin",

    //Look at this, I've even put some dependencies to modules in the same namespace
    // as our PollCreation dijit. I'll get back to these in a second. 
    "whatever/editors/PollDialog",
    "whatever/editors/DeleteableTextBox",
    "dojo/text!./templates/poll.html"
   //This last one is interesting... We actually say here that we have a dependency to a plain html file
   // this file needs to be loaded and put into memory before we can run our module, I'll get back to this one as well.
]

We then pass in our function which has an argument for each of our dependencies:

function (
    connect,
    declare,
    Memory,

    _CssStateMixin,
    _Widget,
    _TemplatedMixin,
    _WidgetsInTemplateMixin,
    FilteringSelect,
    TitlePane,

    dependency,
    epi,
    _ValueRequiredMixin,
    Dialog,
    _HasChildDialogMixin,

    PollDialog,
    DeleteableTextBox,
    template
) {

You could name these arguments anything you want, but it’s good practice to name them the same as the dependency.

Allright, moving on… we then from our function return our declared dijit module:

 return declare("whatever.editors.PollCreation", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin, _HasChildDialogMixin], {
        templateString: template,
        intermediateChanges: false,
        value: null,
        store: null,
        currentPollData: null,

Note how we inherit all functionality from _Widget, _TemplatedMixin, _WidgetsInTemplateMixin etc. especially the _TemplatedMixin is interesting as it allows us to use a HTML template for our module. Remember "dojo/text!./templates/poll.html" from our dependencies? This is where we make use of it. We’ve put the value of the loaded poll.html into the template variable. The path to our poll.html is relative to our module which means that the complete path to the html template is ~/ClientResources/Scripts/Editors/templates/poll.html. All right, so we’ve put the html template into the templateString variable inside our module, which means our module can access that variable using this.templateString which we shall see in practice soon.

Here’s the poll.html template:

<div class="dijitInline">
    <div style="position: relative;">
        <div data-dojo-attach-point="selectPolls" data-dojo-type="dijit.form.FilteringSelect" data-dojo-props="autoComplete: false, required: false" style="width: 400px"></div>
        <span class="epiDeleteIcon" data-dojo-attach-event="click: _onDeletePollClick" style="width:16px; height: 16px; display:inline-block; display: inline-block; margin-left: 5px; position: relative; top: 3px; background: url('../Shell/7.7.0.0/ClientResources/epi/themes/sleek/epi/images/icons/commonIcons16x16.png'); background-position-y: -1168px;" alt=""></span>
        <span data-dojo-attach-point="addButton" data-dojo-attach-event="click: _onButtonClick" style="position:relative; top: 3px; width: 16px; height: 16px; display: inline-block; background: url('../Shell/7.7.0.0/ClientResources/epi/themes/sleek/epi/images/icons/commonIcons16x16.png'); background-position-y: -1216px;"></span>
    </div>
    <div style="margin-left:20px;margin-top:10px;" data-dojo-attach-point="titlePane" data-dojo-type="dijit.TitlePane" data-dojo-props="title:'Answers'">
    </div>
</div>

Yeah, it’s quite a mess but not really that complicated. It’s very important to note that any template that is to be used by a Dijit module must have exactly one root element. If you by mistake add two divs as root element for example the EPiServer forms editing mode will simply fail to load… silently. No errors! Nothing! This can be very frustrating, but as long as you remember to use only one root node you should be fine.

Anyway, lets look at the interesting parts of the template. We have our root node (just a plain old div) and then another div with position relative. Then we have a div with some dojo-markup on it, the data-dojo-attach-point tells our module that we want to reference this div from our JavaScript file using the selectPolls variable and the data-dojo-type attribute tells us that this div is actually a dijit.form.FilteringSelect module and as such has a lot of functionality and properties of it’s own (this is why we need to declare a dependency to the _WidgetsInTemplateMixin, we have widgets in our template). We then have a couple of buttons for deleting and creating a poll, they have data-dojo-attach-event attributes to specify what functions in our module should be called when they are clicked. Finally we have another module which is a dijit.TitlePane that will show our answers.

Like I said, not too complicated, but a lot of steps already and we haven’t even looked at the module in editmode yet!

Lets keep going through our module, next up are all the functions.

//The postCreate function is called directly after our module has been created.
//This is the place to do any intialization that we need. 
postCreate: function () {
           // this.inherited is a function in Dojo that lets us call the base implementation
           // of the currently executing function. Pretty useful!
           this.inherited(arguments);

           //We connect any change events of our filteringselect to our own function defined further down
           // Remember that selectPolls was defined in our poll.html template with a data-dojo-attach-point
           // this is why we have access to it in this context
            this.connect(this.selectPolls, "onChange", this._onInputWidgetChanged);

            //We call our two methods to update the dropdown and answers box
            this._refreshDropDown(this.get('value'));
            this._refreshAnswers(this.get('value'));
        },
       //Pretty self explanatory, returns wether our module is valid or not
       // what's interesting here is that epi will call isValid on every module to make sure the page is valid
       // This is the place to do any validation of the value entered by the user, in our case there won't really be any.
        isValid: function () {
            return this.selectPolls.isValid();
        },
        // Setter for value property
        _setValueAttr: function (value) {
           //When the value of this module is set we also want to set the value of our filteringSelect
            this.selectPolls.set("value", value);
            this._set("value", value);
        },
      // This function is not strictly necessary, but if the module is in readonly mode it could be useful
      // (perhaps the editor is editing the page in a language where this property should not be available)
        _setReadOnlyAttr: function (value) {
            this._set("readOnly", value);
            this.selectPolls.set("readOnly", value);
        },
      //update the value of the module
        _updateValue: function (value) {
            if (this._started && epi.areEqual(this.value, value)) {
                return;
            }

           //We set the value and make sure to refresh the answers box
            this._set("value", value);
            this._refreshAnswers(value);
            this.onChange(value);
        },
      //Create dialog? What's this about?
      //This is where we pop another dialog to let the user create a new poll, it will be called from our buttonClick function
        _createDialog: function () {
            //Here we create a PollDialog which is another module that we haven't built yet, we'll get to it soon enough
            this.pollDialog = new PollDialog({
            });

            //Here we create an epi dialog (which is why we needed a dependency to epi/shell/widget/dialog)
            this.dialog = new Dialog({
                title: "New poll",
                content: this.pollDialog,
                dialogClass: "epi-dialog-portrait"
            });

           //onExecute is triggered when the user clicks OK on the dialog
           //onHide is triggered, well, when the dialog is hidden
           //we got some logic to run when these two occur so we just attach our own event handlers.
            this.connect(this.dialog, 'onExecute', '_onExecute');
            this.connect(this.dialog, 'onHide', '_onDialogHide');
            this.dialog.startup();
        },
        _onDialogHide: function () {
            this.focus();
        },
        _onButtonClick: function () {
           //If the dialog hasn't been previously created we create it
            if (!this.dialog) {
                this._createDialog();
            }
           //Show it! This function is called from the data-dojo-attach-event of our button in poll.html
            this.dialog.show(true);
        },
        _onExecute: function () {           
           //This is triggered when the user clicks OK on the dialog
           // We get the value of our dialog (more on this later when we look at the dialog module)
            var pollData = this.pollDialog.get('value');
            var poll;

           //What? JQuery? Yes, jQuery! You can of course use the Dojo store functionality but I like to have better control over my ajax calls.
            jQuery.ajax({
                url: '/api/polls?pollHeading=' + pollData.pollHeading + '&answers='+pollData.answers,
                type: 'POST',
                contentType: 'application/json; charset=utf-8',
                success: function (result) {
                    poll = JSON.parse(result);
                },
                async: false //We run this synchronously for simplicity here so we don’t have to worry about handling everything in a callback.
            });

            this._refreshDropDown(poll.PollId);
            this.selectPolls.refresh();
        },
        _refreshDropDown: function (selected) {
            var pollData = [];

            jQuery.ajax({
                url: '/api/polls',
                contentType: 'application/json; charset=utf-8',
                success: function (result) {
                    result = JSON.parse(result);

                    //push empty result in case they do not want a poll
                    pollData.push({ name: '', id: -1 });

                    for (var i = 0; i < result.length; i++) {
                        pollData.push({ name: result[i].Header, id: result[i].PollId });
                    }
                },
                async: false
            });

            //This is where we need the dojo/store/Memory dependency we simply create a dojo 
            //store of in memory objects and let that be the store of our FilteredDropdown
            var store = new Memory({
                data: pollData
            });

            this.selectPolls.store = store;
            this.selectPolls.set('value', selected);
        },
        _refreshAnswers: function (selected) {
            this.titlePane.destroyDescendants();
            //destroy any eventual previous answers
            if (selected <= 0)
                return;

            var selectedPoll;

            jQuery.ajax({
                url: '/api/polls/'+selected,
                contentType: 'application/json; charset=utf-8',
                success: function (result) {
                    selectedPoll = JSON.parse(result);
                },
                async: false
            });

            //DeleteableTextBox? It’s a simple module that we’ll create soon
            for (var i = 0; i < selectedPoll.Answers.length; i++) {
                var textBox = new DeleteableTextBox({
                    name: 'text',
                    readOnly: true,
                    value: selectedPoll.Answers[i].Answer,
                    style: 'display:table; margin-bottom:10px;',
                    answerId: selectedPoll.Answers[i].AnswerId
            });
                this.titlePane.addChild(textBox, i);
            }
        },
        _onDeletePollClick: function() {
        //This method should be pretty self explanatory, we delete the poll and refresh 
        //our dropdown and answers
            if (confirm('Are you sure you want to delete this magnificent poll?')) {
                jQuery.ajax({
                    url: '/api/polls/' + this.get('value'),
                    type: 'DELETE',
                    contentType: 'application/json; charset=utf-8',
                    success: function (result) {
                    },
                    async: false
                });

                this._refreshAnswers(-1);
                this._refreshDropDown(-1);
            }
        },
        _onInputWidgetChanged: function(value) {
             //This function is important, if the value in the FilteredDropDown 
             //changes we publish the event to inform EPi that the property has changed.
             this._updateValue(value);
        }
    });

That’s it! But since we haven’t created the DeleteableTextBox and the PollDialog yet we still aren’t done. Lets have a look at the DeleteableTextBox real quick.

define([
"epi",
"dojo",
"dijit/form/TextBox",
"dojo/text!./templates/deleteabletextbox.html"],
function (epi, dojo, TextBox, template) {
    return dojo.declare("whatever.editors.DeleteableTextBox", [TextBox], {
        templateString: template,
        answerId: -1,
        postCreate: function () {
            this.inherited(arguments);
            this.connect(this.clearButton, 'onclick', dojo.hitch(this, function () {
                jQuery.ajax({
                    url: '/api/answers/' + this.answerId,
                    type: 'DELETE',
                    contentType: 'application/json; charset=utf-8',
                    success: function (result) {
                    },
                    async: false
                });
                this.destroy();
            }));
        },
        _valueChanged: function () {
            if (this.onTextChange) {
                this.onTextChange();
            }
        }
    });
});

Now that’s a lot simpler. We just create a simple module that inherits from dijit.form.TextBox and add some functionality for deleting the associated answer. Here’s the html template.

<div class="dijit dijitReset dijitInline dijitLeft" waiRole="presentation">
    <div class="dijitReset dijitInputField dijitInputContainer">
        <div style="float: left;width:100%;">
            <div style="float: left; padding-right: 16px;">
                <input class="dijitReset dijitInputInner" dojoAttachPoint="textbox,focusNode" dojoAttachEvent="onkeyup: _valueChanged" autocomplete="off" />
            </div>
        </div>
        <span class="epiDeleteIcon" dojoAttachPoint="clearButton" style="width:16px; height: 16px; float: left; margin-left: -16px; position:relative; top:3px;" alt=""></span>
        <div style="clear: both; width: 0; height: 0; border: 0; padding: 0; margin: 0;"></div>
    </div>
</div>

All right, almost done. Lets have a look at the dialog.

define([
    "dojo/_base/connect",
    "dojo/_base/declare",

    "dijit/_CssStateMixin",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetsInTemplateMixin",

    "epi/dependency",
    "epi/epi",
    "epi/shell/widget/_ValueRequiredMixin",
    "dojo/text!./templates/polldialog.html"
],
function (
    connect,
    declare,
    _CssStateMixin,
    _Widget,
    _TemplatedMixin,
    _WidgetsInTemplateMixin,
    dependency,
    epi,
    _ValueRequiredMixin,
    template
) {
    return declare("whatever.editors.PollDialog", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin], {
        templateString: template,
        intermediateChanges: false,
        value: null,
        store: null,
        postCreate: function () {
            // call base implementation
            this.inherited(arguments);
        },
        focus: function() {
            this.input.focus();
        },
        parseValues: function() {
            var data = { pollHeading: this.input.get('value'), answers: [] }

            data.answers = $(this.qlist).find('input').map(function() {
                if ($(this).val()) {
                    return $(this).val();
                }
            }).get();

            return data;
        },
        _getValueAttr: function () {
            return this.parseValues();
        }   
    });
});

Pretty straightforward as well. The only thing really worth mentioning is the parseValues function that makes sure that we return a JavaScript object that corresponds to a complete poll, this is why we in the _onExecuted function can get a poll to post from the dialog like this var pollData = this.pollDialog.get('value');.

Here’s the html template for the dialog.

<div>
  <div class="editor-label" style="float: left;" dojoattachpoint="labelContainer">
        <label dojoattachpoint="label">Choose a heading for your poll</label>
    </div>
    <div style="clear: both; height: 10px;"></div>
    <div class="editor-field" dojoattachpoint="inputContainer">
        <div style="width:520px;" data-dojo-type="dijit.form.TextBox" dojoattachpoint="input"></div>
    </div>
    <div style="clear: both;"></div>
    <div style="padding:20px 0 0 20px;">
    <ul dojoattachpoint="qlist">
        <li style="margin-bottom:10px;">Answer 1: <div style="width:437px;" data-dojo-type="dijit.form.TextBox" dojoattachpoint="q1"></div></li>
        <li style="margin-bottom:10px;">Answer 2: <div style="width:437px;" data-dojo-type="dijit.form.TextBox" dojoattachpoint="q2"></div></li>
        <li style="margin-bottom:10px;">Answer 3: <div style="width:437px;" data-dojo-type="dijit.form.TextBox" dojoattachpoint="q3"></div></li>
        <li style="margin-bottom:10px;">Answer 4: <div style="width:437px;" data-dojo-type="dijit.form.TextBox" dojoattachpoint="q4"></div></li>
        <li style="margin-bottom:10px;">Answer 5: <div style="width:437px;" data-dojo-type="dijit.form.TextBox" dojoattachpoint="q5"></div></li>
    </ul>
  </div>
</div>

And now we’re finally done with the plugin. We just need to setup a property on one of our ContentTypes like this.

[ClientEditor(ClientEditingClass = "whatever.editors.PollCreation")]
public virtual int Poll { get; set; }

Ok, lets have a look at the result.

Our property should now look something like this.

Poll

And if we select a poll:

Poll again

Deleting a poll is as simple as clicking on the waste basket icon, and if we click the + we’ll see our dialog in action:

More poll

And that’s it! It took some effort and we could definitely have polished this a bit more, but I hope building custom properties for EPiServer now feels a bit more doable.

You might have noted that I do not have a POST method in my web api controller which means that we’ll get an error when we try to create a new poll. That could be easily remedied by adding a post method, something like this:

public SimplePoll Post(string pollHeading, string answers)
    {
        var poll = new SimplePoll()
        {
            PollId  = 666,
            Header = pollHeading,
            Answers = new List<SimpleAnswer>()
        };

        int i = 500;

        if (string.IsNullOrEmpty(answers))
        {
            return poll;
        }

        var answersArray = answers.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

        foreach (var item in answersArray)
        {
            poll.Answers.Add(new SimpleAnswer() { Answer = item, AnswerId = i++ });
        }
    return poll;
}

However this still won’t work as we do not persist any data. But if you had an underlying database or some other type of persistence it would work as expected.