Tuesday, March 5, 2013

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

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 our requirements.

Although a solution to getting XForms to work inside a block type has been 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.

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 just a simple block type with an XForm property.

[ContentType(GUID = "49754310-E0ED-4C95-AA69-C155323E0AA9")]
public class XFormBlock : BlockData
{
    [Display(GroupName = SystemTabNames.Content)]
    public virtual XForm Form { get; set; }
}

/Models/ViewModels/XFormViewModel.cs


This is just a simple view model that transfers the XForm property and the ActionUrl string to the block's view.

public class XFormViewModel
{
    public XForm Form { get; set; }
    public string ActionUrl { get; set; }
}

/Controllers/BasePageController.cs


This is the most important piece of the puzzle, since it handles all the XForm actions, as well as sets the ViewData that's used in the block controller.

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 XForm methods Success() and Failed() to the page controllers they are redirecting to through the RedirectToAction("Index") call.

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

    public BasePageController()
    {
        _xformHandler = new XFormPageUnknownActionHandler();
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (TempData["ViewData"] != null)
        {
            ViewData = (ViewDataDictionary)TempData["ViewData"];
        }

        base.OnActionExecuting(filterContext);
    }

    protected override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        TempData["ViewData"] = ViewData;

        base.OnResultExecuting(filterContext);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public virtual ActionResult Success(XFormPostedData xFormPostedData)
    {
        return RedirectToAction("Index");
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public virtual ActionResult Failed(XFormPostedData xFormPostedData)
    {
        return RedirectToAction("Index");
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public virtual ActionResult XFormPost(XFormPostedData xFormpostedData)
    {
        return _xformHandler.HandleAction(this);
    }
}

/Controllers/XFormBlockController.cs


In the block's controller, we grab the ViewData that we saved in the BaseController. Then, we instantiate our view model that holds the XForm property and the ActionUrl string for the view.

The important part of this is how we build out the ActionUrl. We can get the currentPage data from the PageRouteHelper, then get the virtual path to the page from the currentPage using a UrlResolver, so we always POST to the page that the block is on. The rest of the values in the query string are to set the actions that the XForm handler uses if the submission was successful or unsuccessful.

public class XFormBlockController : BlockController<XFormBlock>
{
    public override ActionResult Index(XFormBlock currentBlock)
    {
        if (TempData["ViewData"] != null)
        {
            ViewData = (ViewDataDictionary)TempData["ViewData"];
        }

        var viewModel = new XFormViewModel();

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

        if (currentBlock.Form != null && currentPage != null)
        {
            viewModel.Form = currentBlock.Form;

            var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();
            var pageUrl = urlResolver.GetVirtualPath(currentPage.ContentLink);

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

            viewModel.ActionUrl = actionUrl;
        }

        return PartialView(viewModel);
    }
}

/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 XFormViewModel

@Html.ValidationSummary()

@using (Html.BeginXForm(Model.Form, new { Action = Model.ActionUrl }))
{
    Html.RenderXForm(Model.Form);
} 

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.