Home
Atom Feed

Using XForms and MVC in EPiServer 7

UPDATE: Updated blog post to solve issues and questions posted in the comment section, as well as generalizing it to work with XForm instances on pages as well. This version also supports redirection after form submission as well as proper validation.

The most awesome features of EpiServer 7 is arguably it's Block system and that it's possible to use MVC when developing sites.

Alas, some things in EPiServer 7 apparently has some issues when it comes to supporting MVC - XForms being one of them.

For one thing, XForms requires that you post to a url. That means you need to post to an instance of a page, or create a standalone controller for handling xform requests only.

This blog post will show you how to set up XForms using the former method.

 

First off, you need to create a base controller that all your pages must derive from:

public abstract class BasePageController<T> : PageController<T> where T : PageData
{
    /// <summary>
    /// The view data key format
    /// </summary>
    private const string ViewDataKeyFormat = "TempViewData_{0}";

    /// <summary>
    /// Initializes a new instance of the <see cref="BasePageController{T}" /> class.
    /// </summary>
    protected BasePageController()
    {
        this.ContentId = string.Empty;
    }
    
    /// <summary>
    /// Gets or sets the data factory
    /// </summary>
    /// <value>The content repository.</value>
    [SetterProperty]
    public IContentRepository ContentRepository { get; set; }

    /// <summary>
    /// Gets or sets the page route helper repository.
    /// </summary>
    /// <value>The page route helper repository.</value>
    [SetterProperty]
    public PageRouteHelper PageRouteHelperRepository { get; set; }

    /// <summary>
    /// Gets or sets the URL resolver.
    /// </summary>
    /// <value>The URL resolver.</value>
    [SetterProperty]
    public UrlResolver UrlResolver { get; set; }

    /// <summary>
    /// Gets or sets the xform handler.
    /// </summary>
    /// <value>The xform handler.</value>
    [SetterProperty]
    public XFormPageUnknownActionHandler XFormHandler { get; set; }

    /// <summary>
    /// Gets or sets the current page.
    /// </summary>
    /// <value>The current page.</value>
    public PageData CurrentPage { get; set; }

    /// <summary>
    /// Gets a value indicating whether the block [is in edit mode].
    /// </summary>
    /// <returns><c>true</c> if the block [is in edit mode]; otherwise, <c>false</c>.</returns>
    protected bool IsInEditMode
    {
        get
        {
            return PageEditing.GetPageIsInEditMode(this.HttpContext);
        }
    }

    /// <summary>
    /// Gets the view data key.
    /// </summary>
    /// <value>The view data key.</value>
    private string ViewDataKey
    {
        get
        {
            return string.Format(ViewDataKeyFormat, this.ContentId);
        }
    }

    /// <summary>
    /// Gets or sets the content id
    /// </summary>
    private string ContentId
    {
        get
        {
            return this.TempData["ContentID"] != null ? this.TempData["ContentID"].ToString() : (this.TempData["ContentID"] = string.Empty).ToString();
        }

        set
        {
            this.TempData["ContentID"] = value;
        }
    }

    /// <summary>
    /// Handles the XForm postback
    /// </summary>
    /// <param name="data">The posted form data.</param>
    /// <param name="contentId">The block id.</param>
    /// <returns>A view.</returns>
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult XFormPost(XFormPostedData data, string contentId = "")
    {
        if (!string.IsNullOrEmpty(contentId)) 
        {
            this.ContentId = contentId;
        }

        // Add posted xform instance id to viewdata, so we can retrieve it later
        this.ViewData["XFormID"] = data.XForm.Id;

        return this.XFormHandler.HandleAction(this);
    }

    /// <summary>
    /// Handles the XForm postback if it was successful.
    /// </summary>
    /// <param name="data">The posted form data.</param>
    /// <returns>A view.</returns>
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Success(XFormPostedData data)
    {
        if (data.XForm.PageGuidAfterPost != Guid.Empty)
        {
            var page = this.ContentRepository.Get<PageData>(data.XForm.PageGuidAfterPost);

            return this.Redirect(this.UrlResolver.GetVirtualPath(page.ContentLink, page.LanguageBranch));
        }

        return this.Redirect(this.UrlResolver.GetVirtualPath(this.CurrentPage.ContentLink, this.CurrentPage.LanguageBranch));
    }

    /// <summary>
    /// Handles the XForm postback if it was successful.
    /// </summary>
    /// <param name="data">The posted form data.</param>
    /// <returns>A view.</returns>
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Failed(XFormPostedData data)
    {
        return this.Redirect(this.UrlResolver.GetVirtualPath(this.CurrentPage.ContentLink, this.CurrentPage.LanguageBranch));
    }

    /// <summary>
    /// Called before the action method is invoked.
    /// </summary>
    /// <param name="filterContext">Information about the current request and action.</param>
    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // Get current page
        this.CurrentPage = this.PageRouteHelperRepository.Page;

        // Set content id if it has not been set
        if (string.IsNullOrEmpty(this.ContentId))
        {
            this.ContentId = this.CurrentPage.PageLink.ID.ToString();
        }

        // Persist view data if this is a failed or successful xform postback, reset if this is a new postback
        if (filterContext.ActionDescriptor.ActionName == "Failed"
            || filterContext.ActionDescriptor.ActionName == "Success")
        {
            this.TempData[this.ViewDataKey] = this.ViewData;
        }
        else if (filterContext.ActionDescriptor.ActionName == "XFormPost")
        {
            this.TempData[this.ViewDataKey] = null;
        }
        
        // Merge tempdata into viewdata if it is not null
        var tempData = this.TempData[this.ViewDataKey] as ViewDataDictionary;

        if (tempData != null)
        {
            this.ViewData = tempData;
        }

        base.OnActionExecuting(filterContext);
    }
}

 

Making it work with pages

Create a page type class like this:

[ContentType(DisplayName = "XForm Page")]
public class XFormPage
{
    /// <summary>
    /// Gets or sets the form.
    /// </summary>
    /// <value>The form.</value>
    [Display(
        Name = "The form",
        Description = "The form.",
        GroupName = SystemTabNames.Content,
        Order = 1)]
    [CultureSpecific]
    public virtual XForm Schema { get; set; }
}

Now you need to display the form on the page.
Create a ViewModel class for your page type class: 

public class XFormPageViewModel : BasePageViewModel
{
    public XForm Schema { get; set; }

    public string ActionUrl { get; set; }
}

Then create a page controller that looks like this:

public class XFormPageController : BasePageController<XFormPage>
{
    private readonly UrlResolver urlResolver;

    public XFormPageController(UrlResolver urlResolver)
    {
        this.urlResolver = urlResolver;
    }

    public ActionResult Index(XFormPage currentPage)
    {
        var viewModel = new XFormPageViewModel()
                            {
                                Schema = currentPage.Schema
                            };

        if (currentPage.Schema != null)
        {
            var actionUrl = string.Format("{0}XFormPost/", this.urlResolver.GetVirtualPath(currentPage.PageLink, currentPage.LanguageBranch));
            actionUrl = UriSupport.AddQueryString(actionUrl, "XFormId", viewModel.Schema.Id.ToString());
            actionUrl = UriSupport.AddQueryString(actionUrl, "failedAction", "Failed");
            actionUrl = UriSupport.AddQueryString(actionUrl, "successAction", "Success");

            viewModel.ActionUrl = actionUrl;
        }

        var editHints = this.ViewData.GetEditHints<XFormPageViewModel, XFormPage>();
        editHints.AddConnection(v => v.Schema, p => p.Schema);

        return this.View(viewModel);
    }
}

You could avoid using a viewmodel like the one above and just use the page type as a viewmodel, but I like to adhere to the MVVM model and I want to be able to set a custom css class for an instance of the page.

Now we need to create a view. (If you're using ReSharper, just ALT+ENTER on the "return this.View(viewModel)" line)

@using EPiServer.Web.Mvc.Html
@model EPiServer.Demo.TwitterBootstrap.ViewModels.Pages.XFormPageViewModel

<div class="xformpage" @Html.EditAttributes(x => x.Schema)>
    @using (Html.BeginXForm(Model.Schema, new { Action = Model.ActionUrl, @class = "form xform" }))
    {
        Html.RenderXForm(Model.Schema);
    }
    if (Model.Schema.Id == ViewData["XFormID"])
    {
        @Html.ValidationSummary()
    }
</div>

Note: Html.ValidationSummary() is wrapped in an if statement to prevent the validation summary from showing errors from other forms on the same page.

Now you're ready to go. Just create an instance of a XFormPage, add a form to it and try i out. 

 

Making it work with Blocks

This is very similar to making it work with pages, you just need one more parameter on the action url.

First off, you need to create your Block:

[ContentType(DisplayName = "XForm block")]
public class XFormBlock : BlockData
{
    /// <summary>
    /// Gets or sets the form.
    /// </summary>
    /// <value>The form.</value>
    [Display(
        Name = "The form",
        Description = "The form.",
        GroupName = SystemTabNames.Content,
        Order = 1)]
    [CultureSpecific]
    public virtual XForm Schema { get; set; }
}

And the corresponding viewmodel:

public class XFormBlockViewModel
{
    /// <summary>
    /// Gets or sets the form.
    /// </summary>
    /// <value>The form.</value>
    public XForm Schema { get; set; }

    /// <summary>
    /// Gets or sets the action URI.
    /// </summary>
    /// <value>The action URI.</value>
    public string ActionUri { get; set; }
}

Then you need to create a controller for showing the block on a page:

public class XFormBlockController : BlockController<XFormBlock>
{
    private readonly PageRouteHelper pageRouteHelper;

    private readonly UrlResolver urlResolver;

    public XFormBlockController(PageRouteHelper pageRouteHelper, UrlResolver urlResolver)
    {
        this.pageRouteHelper = pageRouteHelper;
        this.urlResolver = urlResolver;
    }

    public override ActionResult Index(XFormBlock currentBlock)
    {
        var id = (currentBlock as IContent).ContentLink.ID;

        // ViewData is not automatically passed to a block controller, need to get it from TempData if it exists
        var viewDataKey = string.Format("TempViewData_{0}", id);

        if (this.TempData[viewDataKey] != null)
        {
            this.ViewData = (ViewDataDictionary)this.TempData[viewDataKey];
        }

        // Create the viewmodel
        var viewModel = new XFormBlockViewModel()
                            {
                                Schema = currentBlock.Schema
                            };

        // Create postback url
        if (viewModel.Schema != null && this.pageRouteHelper.Page != null)
        {
            var actionUrl = string.Format("{0}XFormPost/", this.urlResolver.GetVirtualPath(this.pageRouteHelper.Page.PageLink, this.pageRouteHelper.Page.LanguageBranch));
            actionUrl = UriSupport.AddQueryString(actionUrl, "XFormId", viewModel.Schema.Id.ToString());
            actionUrl = UriSupport.AddQueryString(actionUrl, "failedAction", "Failed");
            actionUrl = UriSupport.AddQueryString(actionUrl, "successAction", "Success");
            actionUrl = UriSupport.AddQueryString(actionUrl, "contentId", id.ToString());

            viewModel.ActionUri = actionUrl;
        }

        var editHints = this.ViewData.GetEditHints<XFormBlockViewModel, XFormBlock>();
        editHints.AddConnection(v => v.Schema, p => p.Schema);

        return this.PartialView(viewModel);
    }
}

When that's done, create the partial view for the block:

@using EPiServer.Web.Mvc.Html
@model EPiServer.Demo.TwitterBootstrap.ViewModels.Partials.XFormViewModel

<div class="xformblock" @Html.EditAttributes(x => x.Schema)>
    @using (Html.BeginXForm(Model.Schema, new { Action = Model.ActionUri, @class = "form xform" }))
    {
        Html.RenderXForm(Model.Schema);
    }
    if (Model.Schema.Id == ViewData["XFormID"])
    {
        @Html.ValidationSummary()
    }
</div>

UPDATE: There is an issue with using the built-in XForm display template and multiple XForm blocks on the same page. Basically, it will replace the contents of all other forms with the content of the posted form on postback.

To solve this, put the following code in a file named XForm.cshtml in a folder called DisplayTemplates in your ~/Views/Shared folder:

@using EPiServer.HtmlParsing
@using Leroy.Web.EPiServer.BusinessLogic.Extensions
@model EPiServer.XForms.XForm
           
@if (Model != null)
{
    var fragments = Model.CreateHtmlFragments();

    // Check if this is a postback, and if it is get the posted fragments for this instance
    if (ViewData["XFormID"] != null && Model.Id == ViewData["XFormID"])
    {
        fragments = (IEnumerable<HtmlFragment>)ViewData["XFormFragments"];
    }
    
    // Render all fragments
    foreach (var fragment in fragments)
    {
        @Html.RenderCustomHtmlFragment(fragment, "XForm/" + Model.Id + "/" + fragment.GetType().Name)
    }
}

That is all. Now you just need to add your XFormBlock to a page using a ContentArea, and the XFormBlockController will take care of rendering the form.

When the user submits the form, the BasePageController will take care of storing the data and/or sending emails, etc.

Stay tuned for the next blog post where I'll show you how to completely customize the rendering of the form based on the built-in MVC rendering engine. :-)