Skip to main content

Architecture

Dynamic Product Detail Pages. Part 3 – WFFM

Dynamic Lights@1x.jpg

[su_note note_color=”#fafafa”]In part 1 of this series we built a functional prototype of a dynamic product detail page. In part 2 we componentized it and learned to share product data with all components on the page. It’s now a fully functional solution but it’s not yet complete.[/su_note]

Web Forms For Marketers

It’s not uncommon to have some sort of a lead generation form on your product detail page. While you sure can code the form yourself, the marketing team would probably prefer to have more control over that form and the submission data. Many third-party marketing automation platforms support forms but you don’t really need one if you already have Sitecore – it comes with Web Forms For Marketers (aka WFFM) module.

WFFM has two main parts to it – Form Builder and Form Renderer. The builder is basically a Content Editor wizard that allows you to build your form, customize it to your liking, and review submissions. The renderer, as the name suggests, takes care of rendering the form on the page and processing submits.

MVC

WFFM supports MVC as of version 2.4 (the most recent 2.5 is for Sitecore 7.5+ and it uses a MongoDB collection instead of a SQL database to store submission data). The form builder in 2.4+ is most likely the same as in earlier versions. What changed is the rendering that now comes in two versions – a webcontrol for Web Forms solutions and a controller rendering for MVC.

I need to tell you a little about how Sitecore.Forms.Mvc.* is implemented to later walk you through the customizations that we need to make to support a form on a dynamic (no item behind it) page.

The developers who built WFFM MVC decided to use dependency injection for the controller. Their approach was to make it unobtrusive for the surrounding Sitecore so they defined a custom Renderer that uses its own controller Runner that uses Simple Injector when instantiating the controller. They’re also using their own model binder and model metadata provider. The latter is not relevant to this blog post but I will get back to it in my next article about WFFM MVC.

The form rendering itself is using SitecoreController under the hood to generate the form’s action and route it back to the current page item to receive a POST. There’s also a custom clientevent/{action} route that receives analytics tracking signals (to track abandoned forms, for example, it records every value a user types into a field). A few code snippets (simplified) to illustrate:

Config patches (showing only what’s relevant to this post):

<initialize>
<processor type="Sitecore.Forms.Mvc.Pipelines.Routes.InitializeRoutes, Sitecore.Forms.Mvc" />
<processor type="Sitecore.Forms.Mvc.Pipelines.AddCustomMetadataProvider, Sitecore.Forms.Mvc" />
</initialize>

<mvc.getRenderer>
<processor type="Sitecore.Forms.Mvc.Pipelines.GetFormControllerRenderer, Sitecore.Forms.Mvc"/>
</mvc.getRenderer>

Controller Rendering:

public class FormController : SitecoreController, IHasModelFactory, IHasFormDataManager
{
public FormController(IModelFactory modelFactory, IFormDataManager formDataManager)
{
// ...
}

[HttpGet]
public override ActionResult Index()
{
// ...
return this.View(model);
}

[HttpPost]
public virtual ActionResult Index([ModelBinder(typeof (FormModelBinder))] FormModel form)
{
// ...

<dl>
<dt>return redirectPage == null</dt>
<dt>? this.View(form)</dt>
<dd>this.RedirectToRoute(MvcSettings.SitecoreRouteName, new
{
pathInfo = LinkManager.GetItemUrl(redirectPage)
});
}
}

Editor template for the form:

@using (Html.BeginRouteForm(MvcSettings.SitecoreRouteName, FormMethod.Post))
{
// ...
@Html.Sitecore().FormHandler()
// ...
}

Form on a Dynamic Page

When you drop the Form rendering onto the dynamic page template it will show the form on a product detail page just fine but it will render the site’s home page upon submission (non-AJAX) or just do nothing (AJAX). Why is that?

The form’s action will be “/” which explains the home page side effect but why is it a “/“? Well, this is how SitecoreController works. I blogged about it. SitecoreRouteName is a match-all {*pathInfo} route and when used in the Html.BeginRouteForm() helper it would normally render the path to the current context item. We don’t have one, remember? That’s why we have a “/“. And the AJAX doesn’t seem to do anything for exactly the same reason. It does POST back to the home page and the WFFM JavaScript just can’t find the “Thank You” message in the landing page markup. We will get to the AJAX part in a minute.

We clearly need to:

  • Route the form submission elsewhere, somewhere where we can process it
  • Delegate to the WFFM controller logic once we captured the submission
  • Ensure nobody (WFFM, Sitecore, MVC, JavaScript) understands what is happening

Re-Route

There is a way to re-route the form submission without modifying WFFM renderings. See that @Html.Sitecore().FormHandler() in the code snippets I posted above? I mentioned it in my Sitecore MVC routing blog post. I honestly don’t know why WFFM developers put it in there as it’s of no use to the controller rendering (only view renderings have the Form Controller Name and Form Controller Action fields) but it’s there and we can use it. With the same container rendering technique that I showed you in part 2 we can create a special WFFM forms routing container (it will be a controller rendering):

public class WffmSubmissionRoutingContainer : Controller
{
[HttpGet]
public ActionResult Container()
{
var page = PageContext.Current.PageDefinition;
var rendering = RenderingContext.Current.Rendering;

var forms = page.Renderings
.Where(r =&amp;amp;gt; r.Placeholder.StartsWith(rendering.Placeholder + "/formcontainer"))
.Where(r =&amp;amp;gt; r.RenderingItem.ID == IDs.FormMvcInterpreterID);

foreach (var form in forms)
{
form["Form Controller Name"] = "WffmSubmissionReceiver";
form["Form Controller Action"] = "Submit";
}

return View();
}
}

And the view:

&amp;amp;lt;div class="mysite-form-container"&amp;amp;gt;
@Html.Sitecore().Placeholder("formcontainer")
&amp;amp;lt;/div&amp;amp;gt;

Every WFFM form rendering that is put into the formcontainer will receive routing instructions and will render proper scController and scAction hidden fields that will route the POST to our custom WffmSubmissionReceiver controller.

Delegate

Our controller will receive the WFFM form submission and we now need to delegate the processing back to the WFFM logic. It would be easy if not for the custom model binder that I mentioned earlier. The WFFM binder assumes it runs in context of a controller rendering and thus thinks it can access the rendering context (which we don’t have in case of a routed POST request). We will need to stage that. The binder is also rather tightly coupled to the controller as it assumes it can delegate model creation to it via IHasModelFactory interface (you can look into WFFM’s FormModelBinder to learn more). Let’s handle that as well:

public class WffmSubmissionReceiverController : Controller, IHasModelFactory, IHasFormDataManager
{
private readonly FormController _wffm;

public WffmSubmissionReceiverController()
{
_wffm = SimpleInjectorControllerFactory.GetController(typeof (FormController)) as FormController;
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Submit([ModelBinder(typeof (FormModelBinderAdapter))] FormModel form)
{
// WFFM accesses HttpContext, Request, and Session via ControllerContext

_wffm.ControllerContext = this.ControllerContext;

// ASP.NET MVC must think it's running the original _wffm controller
// otherwise it won't find the views and other things

_wffm.ControllerContext.RouteData.Values["controller"] = "Form";

// We need to send it to a special wrapping view to overcome a particular JS assumption
// that WFFM makes about what it will receive back.
// It expects full page and jQuery success message lookup won't work without a wrapping markup

_wffm.ControllerContext.RouteData.Values["action"] = "IndexStandalone";

// SitecoreFormsProcessor needs RenderingContext to set Form.PageId.
// The construct below will set RenderingContext.Current.ContextItem to the home page item
// which is the item this controller runs in context of (re-routed form posts to "/" so that's why)
// Form.PageId is used to send client-side event tracking info (see &amp;amp;lt;script&amp;amp;gt; in /Views/Form/Index.cshtml)
// There's no tracking after the form is submitted so it's OK to have it point to the home page.

using (RenderingContext.EnterContext(new Rendering()))
{
return _wffm.Index(form);
}
}

public IModelFactory ModelFactory
{
get { return _wffm.ModelFactory; }
}

public IFormDataManager FormManager
{
get { return _wffm.FormManager; }
}
}

And a wrapping model binder:

public class FormModelBinderAdapter : FormModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ID formId = IDUtil.GetFormId(controllerContext.HttpContext.Request.Form);

// We need to temporarily stage the rendering context as WFFM MVC is designed to work
// as a controller rendering.
// The only thing it will look at when binding a model is the Datasource which is the form item

using (RenderingContext.EnterContext(new Rendering { DataSource = formId.ToString() }))
{
return base.BindModel(controllerContext, bindingContext);
}
}
}

View

Still with me? I know it’s a lot. I spent a few good hours navigating the WFFM, Sitecore, and MVC to figure it all out. Take your time, we are almost there.

We re-routed the form to our own controller. We made our controller receive the POST and taught it how to bind a model. Yet we haven’t done anything – we delegated the doing back to WFFM with properly staged contexts. It would have been all we had to do and we wouldn’t need a custom IndexStandalone view if WFFM didn’t cheat with the AJAX forms. You heard it right, they cheated.

You would think an AJAX form does a lightweight communication in the background and expects some kind of JSON response back, right? Not if you are a WFFM AJAX form. It posts back to an item so technically Sitecore will re-render the item and will send back a fully rendered page. The WFFM’s controller rendering can tell GET vs. POST and that’s how it will know whether to process the submission or render the blank form. And then in the view within the <form> they do something like this:

if (Model.SuccessSubmit)
{
@Html.Encode(Model.SuccessMessage)
return;
}

The JavaScript will then take the “Thank You” message out of the markup and replace the form on the page with it:

$scw.ajax({
url: this.action,
type: this.method,
// ...
success: function (result) {
// [Note]: options.targetId is the &amp;amp;lt;form&amp;amp;gt; ID
$scw('#' + options.targetId).html($scw(result).find("#" + options.targetId).get(0).outerHTML);
},
error: function (xhr, status, exception) {
$scw('#' + options.targetId).html(xhr.responseText);
}
});

It’s basically looking for the <form> with the same ID to grab its content and replace the form it has on the screen with it. Clever. Still, I think it’s called cheating. I will get back to it in my next WFFM article as this technique has side effects.

Our controller can’t re-render the page like the WFFM does normally. We lost that context when we routed away from the item that we anyway don’t have so all we can render is the form itself. That markup starts with the <form> tag and jQuery’s find() won’t find it. Unfortunately it’s not descendants-or-self::*, it’s just the descendants::*. That’s why we need a special wrapping IndexStandalone.chtml view that we’ll put into /Views/Forms:

@model Sitecore.Forms.Mvc.Models.FormModel
&amp;amp;lt;div&amp;amp;gt;
@Html.Partial("Index", Model);
&amp;amp;lt;/div&amp;amp;gt;

That’s it. Just add a wrapping <div>. I say we, too, know how to cheat.

Summary

To make WFFM forms work on dynamic pages you need to re-route the submission, accept the POST and delegate the processing back to WFFM logic, stage contexts as required and assumed by the code that didn’t expect to be re-routed or rendered outside of normal rendering context, and wrap the rendered form markup into a <div> to keep WFFM’s JavaScript happy.

Side effects:

  • Can’t report server-side validation errors in a non-AJAX form. While we can render the form back (and it will work just fine with AJAX) we can’t render the full page back for traditional POST forms.
  • The WFFM analytics tracking that I mentioned in the beginning will create page events attached to the template page item. I mentioned it in previous articles and we will get back to it in part 4

Coming Up

I feel like writing two more posts about Web Forms For Marketers with MVC before I close this series with a part 4 article. Here’s what’s coming up:

  • WFFM hurt My Rendering Model – a post about a wrinkle in WFFM MVC implementation that conflicts with Sitecore MVC’s Rendering Model.
  • WFFM with MVC: Implementation Tips – a summary of nuances, implementation details, and issues that you may want to know if you’re about to implement WFFM in your Sitecore MVC project
  • Dynamic Product Detail Pages. Part 4 – a look at analytics, SEO, and performance implications of building dynamic (no item behind it) product pages in Sitecore

Stay tuned!

Thoughts on “Dynamic Product Detail Pages. Part 3 – WFFM”

  1. Thanks for the deep dive on this issue. We came across this when we switched to WFFM and your blog was instrumental on crafting the solution. Thanks!

  2. Nice post, I was trying to use your code but I’m unable to find some classes such as SimpleInjectorControllerFactory or IHasModelFactory/IModelFactory. I was wondering what version of WFFM are you using and if other libraries are required.

    Thanks

  3. Hey Christian, I honestly don’t exactly remember. It was before 8, that much I am certain of. WFFM MVC was definitely updated after that so it’s likely that later versions exhibit different behavior and/or have different dependencies.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Brian Beckham

As a Sitecore MVP, Brian spends most of his time consulting and architecting software solutions for enterprise-level Sitecore projects.

More from this Author

Follow Us