Creating a XForm Block in EPiServer 7 MVC with Working Validation (Updated)

on April 11, 2013 in Episerver, Episerver 7, Episerver MVC, XForms

I initially posted this solution on the Nansen blog, but since then the code has been updated for both bug fixes and to reduce unnecessary layers (at least, unnecessary for my situation).

With the flexibility that block types offer in EPiServer 7, it would make sense that one common feature that people would want is an XForm in a block, so editors can freely add, move, and reuse blocks with forms around their website. In a recent project, this was one of my requirements.

Although a solution for making XForms to work inside a block type has been partially solved, it doesn't fully support validation, which is a common requirement. Also, the documentation is still lacking when it comes to explaining how to integrate XForms in EPiServer 7 MVC, both within a page type and a block type.

This solution was developed using the initial release of EPiServer 7. The EPiServer support team has stated that they may implement a solution for fully working XForms in a block on a future release.

The Solution

The solution to this revolved heavily around making sure the action URL on the form was correct (so we stay on the page the the block is located) and sharing the controller's ViewData with all controllers that needed it (both page controllers and block controllers).

/Models/Blocks/XFormBlock.cs

This is the block class with the XForm block. By decorating the ActionUri property with the [Ignore] attribute, we can treat this class also as the view model for the view. The [Ignore] attribute prevents EPiServer from registering the property as an editable block type property.

[ContentType(GUID = "00000000-0000-0000-0000-000000000000")]
public class XFormBlock : BlockData
{
    [Display(GroupName = SystemTabNames.Content)]
    public virtual XForm Form { get; set; }

    [Ignore]
    public virtual string ActionUri { get; set; }
}

/Controllers/BasePageController.cs

This is the most important piece of the solution, since it handles all the XForms actions, as well as sets the ViewData that's used in the block controller. The controller for any page that could potentially have an XForm block should inherit this base controller class.

The first main method that we override is the OnResultExecuting() method, which sets the ViewData in the TempData collection after a page controller sets it. Without this, the block controller will have a different ViewData, which makes all the validation information go away.

The second main method we override is OnActionExecuting(), which handles the transfer of the ViewData between the XForms methods Success() and Failed() to the page controllers they are redirecting to through the RedirectToAction("Index") call.

When the ViewData is set, we use a unique key for the form in the TempData collection. The unique key uses the ContentLink ID of the block, which is built into the ActionUri and passed in as a parameter to the XFormPost method. This unique key allows us to use multiple XForms blocks on the same page without the ViewData of one block interfering with the ViewData of another block. The only exception to this is when a page has two instances of the same XForms block (which really doesn't happen very often). One side benefit of this is that we only transfer the ViewData when we have the _contentId, which prevents unnecessary saving and fetching of the ViewData.

public class BasePageController<T> : PageController<T> where T : PageData
{
    private readonly XFormPageUnknownActionHandler _xformHandler;

    private string _contentId;

    private readonly string _viewDataKeyFormat = "ViewData_{0}";
    private string ViewDataKey
    {
        get
        {
            return string.Format(_viewDataKeyFormat, _contentId);
        }
    }

    public BasePageController()
    {
        _xformHandler = new XFormPageUnknownActionHandler();
        _contentId = string.Empty;
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!string.IsNullOrEmpty(_contentId))
        {
            if (TempData[ViewDataKey] != null)
            {
                ViewData = (ViewDataDictionary)TempData[ViewDataKey];
            }
        }

        base.OnActionExecuting(filterContext);
    }

    protected override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (!string.IsNullOrEmpty(_contentId))
        {
            TempData[ViewDataKey] = ViewData;
        }

        base.OnResultExecuting(filterContext);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public virtual ActionResult Success(XFormPostedData xFormPostedData)
    {
        return RedirectToAction("Index", new { language = PageContext.LanguageID });
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public virtual ActionResult Failed(XFormPostedData xFormPostedData)
    {
        return RedirectToAction("Index", new { language = PageContext.LanguageID });
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public virtual ActionResult XFormPost(XFormPostedData xFormpostedData, string contentId)
    {
        _contentId = contentId;

        return _xformHandler.HandleAction(this);
    }
}

/Controllers/XFormBlockController.cs

In the block's controller, we grab the ViewData that we saved in the BasePageController using the same unique key. This allows us to maintain the validation data for the XForm.

The important part of this is how we build out the ActionUrl. We can get the currentPage data from the PageRouteHelper, then find the virtual path to the page we are viewing using a UrlResolver, so we always POST to the page that the block is on. We also put the block's ContentLink ID in the ActionUri so the BasePageController knows how to save the ViewData. The rest of the values in the query string are to set the actions that the XForms handler uses if the submission was successful or unsuccessful.

One things to note is that if we are using on-page editing for the block, we won't have a currentPage. In this situation, we just borrow the Start Page. This won't matter anyway, because if you try to click the "Submit" button, rather than the form being submitted, the form selection modal will pop up instead.

public class XFormBlockController : BlockController<XFormBlock>
{
    public override ActionResult Index(XFormBlock currentBlock)
    {
        var currentBlockID = (currentBlock as IContent).ContentLink.ID;
        var viewDataKey = string.Format("ViewData_{0}", currentBlockID);

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

        var pageRouteHelper = ServiceLocator.Current.GetInstance<PageRouteHelper>();
        PageData currentPage = pageRouteHelper.Page;

        // For block preview mode, we need to have a current page, 
        // but since preview isn't really a page, we'll use the start page
        // This won't matter anyway. If you try to submit the form,
        // the form selection window will pop up.
        if (currentPage == null)
        {
            var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
            currentPage = contentLoader.Get<StartPage>(ContentReference.StartPage);
        }

        if (currentBlock.Form != null && currentPage != null)
        {
            var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();
            var pageUrl = urlResolver.GetVirtualPath(currentPage.ContentLink);

            var actionUri = string.Format("{0}XFormPost/", pageUrl);
            actionUri = UriSupport.AddQueryString(actionUri, "XFormId", currentBlock.Form.Id.ToString());
            actionUri = UriSupport.AddQueryString(actionUri, "failedAction", "Failed");
            actionUri = UriSupport.AddQueryString(actionUri, "successAction", "Success");
            actionUri = UriSupport.AddQueryString(actionUri, "contentId", currentBlockID.ToString());

            currentBlock.ActionUri = actionUri;
        }

        return PartialView(currentBlock);
    }
}

/Views/XFormBlock/Index.cshtml

This is just a simple view for the block. The primary thing that is different compared to how we normally output page properties is that we need to set the action on the form.

@model XFormBlock

<div @Html.EditAttributes(m => m.Form)>
    @Html.ValidationSummary()
    @using (Html.BeginXForm(Model.Form, new { Action = Model.ActionUri }))
    {
        Html.RenderXForm(Model.Form);
    }
</div>

And that's it. With this solution, you don't need to worry handling the XForm in any specific page controllers that the block lives in, although you could easily just override the XFormPost() method in the BasePageController if needed. This also supports the built-in 'Save to database' and/or 'Send e-mail' submit options.