Working with your own forms in Episerver and MVC

Custom forms is a common thing to have in a site's templates.

In the older Web Forms days this was typically done by adding regular ASP.NET Web Controls and checking IsPostBack() inside OnLoad() in the same page template. If posting was done from a plain HTML form the standard in my experience was to post to the same page and use Request.Form or Request.QueryString to get the form values inside OnLoad().

With MVC I haven't seen that many different Episerver implementations yet and I don't find that many articles on what people are doing so I thought I'd post my thoughts.

Firstly the ASP.NET MVC HTML Helpers for Forms turns my stomach inside out. I know HTML forms, so why not let me use that knowledge and keep full control of the markup? Retaining form element values is after all not that much work.

The following is a lab I made that supports model binding, keeping form values and full markup control.

The lab site is a default Alloy MVC site from the Episerver Visual Studio extension upgraded to 7.16.1.

I started by creating a new Page Type called CustomFormPage and the following model that matches my demo form HTML code to work with the model binding and a simple file upload.

namespace playground_epi76_mvc.Models.CustomForms
{
    using System.Web;

    public class CustomFormModel
    {
        public string Action { get; set; }

        public string Email { get; set; }

        public HttpPostedFileBase File { get; set; }

        public string Name { get; set; }

        public string Radios { get; set; }

        public string[] CheckBoxes { get; set; }

        public string CheckBox { get; set; }

        public string SelectBasic { get; set; }

        public string TextArea { get; set; }
    }
}

I also needed a ViewModel.

namespace playground_epi76_mvc.Models.ViewModels
{
    using playground_epi76_mvc.Models.CustomForms;
    using playground_epi76_mvc.Models.Pages;

    public class CustomFormContentModel : PageViewModel<CustomFormPage>
    {
        public CustomFormContentModel(CustomFormPage currentPage)
            : base(currentPage)
        {
            this.CustomForm = new CustomFormModel();
        }

        public CustomFormModel CustomForm { get; set; }

        public string MessageOnPost { get; set; }
    }
}

And a Controller. Note that an alternative to this for some forms and types could be to put a matching action in the DefaultPageController instead, it depends on if you need a specific ViewModel or not.

namespace playground_epi76_mvc.Controllers
{
    using System;
    using System.IO;
    using System.Web.Mvc;
    using playground_epi76_mvc.Models.CustomForms;
    using playground_epi76_mvc.Models.Pages;
    using playground_epi76_mvc.Models.ViewModels;

    public class CustomFormPageController : PageControllerBase<CustomFormPage>
    {
        public ActionResult Index(CustomFormPage currentPage)
        {
            var model = new CustomFormContentModel(currentPage);
            return View(model);
        }

        [HttpPost]
        public ActionResult Index(CustomFormPage currentPage, CustomFormModel postedForm)
        {
            var model = new CustomFormContentModel(currentPage) { CustomForm = postedForm };

            if (postedForm.File != null)
            {
                string path = Path.Combine("c:\\Temp\\", Path.GetFileName(postedForm.File.FileName));
                postedForm.File.SaveAs(path);
            }

            model.MessageOnPost = string.Format("The form was posted at {0}", DateTime.Now);

            return View(model);
        }
    }
}

Debugging shows that the binding works as expected.

Binding worked

To have something to work with I acquired some Bootstrap HTML even though it's bloated and below average in most aspects. For a lab I can live with it. I added the most common element types including a file input.

@using Microsoft.Ajax.Utilities
@model CustomFormContentModel

@{ Layout = "~/Views/Shared/Layouts/_LeftNavigation.cshtml"; }

<h1 @Html.EditAttributes(x => x.CurrentPage.PageName)>@Model.CurrentPage.PageName</h1>

<p class="introduction" @Html.EditAttributes(x =>
  x.CurrentPage.MetaDescription)>@Model.CurrentPage.MetaDescription</p>

@Html.Raw(Model.MessageOnPost.IsNullOrWhiteSpace() ? ""
: "<div class=\"alert alert-success\"><a href=\"#\" class=\"close\" data-dismiss=\"alert\">&times;</a>"
 + Model.MessageOnPost + "</div>")

<form class="form-horizontal" action="." method="POST" enctype="multipart/form-data">
    <fieldset>
        <legend>Custom Form Legend</legend>

        <div class="control-group">
            <label class="control-label" for="email">E-mail</label>
            <div class="controls">
                <input id="email" name="email" class="input-xlarge" type="text" value="@Model.CustomForm.Email">
            </div>
        </div>

        <div class="control-group">
            <label class="control-label" for="name">Name</label>
            <div class="controls">
                <input id="name" name="name" class="input-xlarge" type="text" value="@Model.CustomForm.Name">
            </div>
        </div>

        <div class="control-group">
            <label class="control-label" for="selectbasic">Select Basic</label>
            <div class="controls">
                <select id="selectbasic" name="selectbasic" class="input-xlarge">
                    <option value="1" @Html.Raw(Model.CustomForm.SelectBasic == "1"
                      ? " selected=\"selected\"" : "")>Option one</option>
                    <option value="2" @Html.Raw(Model.CustomForm.SelectBasic == "2"
                      ? " selected=\"selected\"" : "")>Option two</option>
                </select>
            </div>
        </div>

        <div class="control-group">
            <label class="control-label" for="radios">Multiple Radios</label>
            <div class="controls">
                <label class="radio" for="radios-0">
                    <input name="radios" id="radios-0" value="1" @Html.Raw(Model.CustomForm.Radios != "2"
                      ? " checked=\"checked\"" : "") type="radio">
                    Option one
                </label>
                <label class="radio" for="radios-1">
                    <input name="radios" id="radios-1" value="2" @Html.Raw(Model.CustomForm.Radios == "2"
                      ? " checked=\"checked\"" : "") type="radio">
                    Option two
                </label>
            </div>
        </div>

        <div class="control-group">
            <label class="control-label" for="textarea">Text Area</label>
            <div class="controls">
                <textarea id="textarea" name="textarea">@Model.CustomForm.TextArea</textarea>
            </div>
        </div>

        <div class="control-group">
            <label class="control-label" for="checkboxes">Multiple Checkboxes</label>
            <div class="controls">
                <label class="checkbox" for="checkboxes-0">
                    <input name="checkboxes" id="checkboxes-0"
                       value="1" @Html.Raw(Model.CustomForm.CheckBoxes != null
                       && Model.CustomForm.CheckBoxes.Contains("1") ? " checked=\"checked\"" : "")
                       type="checkbox">
                    Option one
                </label>
                <label class="checkbox" for="checkboxes-1">
                    <input name="checkboxes" id="checkboxes-1"
                       value="2" @Html.Raw(Model.CustomForm.CheckBoxes != null
                       && Model.CustomForm.CheckBoxes.Contains("2") ? " checked=\"checked\"" : "")
                       type="checkbox">
                    Option two
                </label>
            </div>
        </div>

        <div class="control-group">
            <label class="control-label" for="filebutton">File Button</label>
            <div class="controls">
                <input id="filebutton" name="file" class="input-file" type="file">
            </div>
        </div>

        <div class="control-group">
            <div class="controls">
                <label class="checkbox" for="checkbox">
                    <input name="checkbox" id="checkbox" value="1"
                      type="checkbox" @Html.Raw(Model.CustomForm.CheckBox == "1"
                      ? " checked=\"checked\"" : "")>
                    Yes, I accept
                </label>
            </div>
        </div>

        <div class="control-group">
            <div class="controls">
                <input type="hidden" name="action" value="submit" />
                <button id="singlebutton" class="btn btn-primary" type="submit">Button</button>
            </div>
        </div>

    </fieldset>
</form>

Page screenshot

I'm pretty happy to work in this fashion. An obvious addition would be to decorate the form model properties with with [Required] and similar to be able to do a IsValid() when posting.

Feedback is appreciated.

Published and tagged with these categories: Episerver, ASP.NET, Development, MVC