DEV Community

Cover image for Kentico EMS: MVC Widget Experiments Part 3 - Rendering Form Builder Forms Without Widgets
Sean G. Wright
Sean G. Wright

Posted on • Updated on

Kentico EMS: MVC Widget Experiments Part 3 - Rendering Form Builder Forms Without Widgets

Widget Experiments

This series dives into Kentico 12 MVC Widgets and the related technologies that are part of Kentico's Page Builder technology - Widget Sections, Widgets, Form Components, Inline Editors, and Dialogs 🧐.

Join me 👋, as we explore the nooks and crannies of Kentico EMS MVC Widgets and discover what might be possible with this powerful technology...

If you aren't yet familiar with Kentico MVC Widgets, check out Kentico's Youtube video on Building a Page with MVC Widgets in Kentico.

The technique described below is no longer necessary in Kentico Xperience 13, which fully supports rendering widgets in code.

Goals

This post is going to explore a way we can render a Form Builder Form statically, without Widgets or the Page Builder 😎.

Note: The core of the approach discussed below came from Lee Conlin's post 👍 Using forms in Kentico 12 MVC without the page builder, but I think my example is even simpler.

Use Case - Displaying a Form in the Page Footer

Imagine a content manager of our Kentico 12 MVC site wants a "Contact Us" form, managed by Kentico's Form Builder feature, to appear in the footer of the site.

Well, that's not a problem 😉! Thinking back to my last post, we remember that MVC Editable Areas (and therefore Widgets) can be placed in the _Layout.cshtml of the app.

We also know that we can define an Editable Area specifically for Forms by using the @Html.Kentico().FormArea("...") extension method in our View 😄.

Perfect!

We open our _Layout.cshtml and drop the Form Area right in the footer.

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... -->
</head>
<body>
    <!-- ... -->

    @RenderBody()

    <footer>
      @Html.Kentico().FormArea("layout-footer-form")
    </footer>

    <!-- ... -->
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

All done, right 🙄?

Problem - Page Builder's Single Page Context

Unfortunately this solution doesn't really solve our problem. The Content Manager wants the same form in the footer for the entire site.

Sure, they could add a form via the Page Builder interface in the CMS for a couple pages, but if the site has 100 pages... or 1,000 pages 😱?

The issue here is that every MVC Widget added to a page is only added to the Editable Area on that specific page.

Even if the Editable Area is defined in a place that appears on every page (like the <footer>), it still has to be populated with Widgets (including our Form Widget) on a per-page basis 🤔.

This is good in some circumstances, like when we want pages to be independent - editing Widgets on one page doesn't affect the Widgets on any other page.

But in this specific circumstance we want the exact same form displayed on every single page without any additional work by the Content Manager 😫.

Solution - Render Using Built-in Kentico APIs

We're going to solve this problem by rendering the form ourselves using the APIs that Kentico exposes.

Since we won't be using the Page Builder functionality, the selection and rendering of the Form won't be page-dependent 💪🏾.

Render Using Child Actions

First, to get the Form to render in the _Layout.cshtml, we're going to need to use a Child Action.

Going back to our Layout markup, we can change it to the following:

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... -->
</head>
<body>
    <!-- ... -->

    @RenderBody()

    <footer>
      @{ Html.RenderAction(
           actionName: "Form", 
           controllerName: "Form", 
           routeValues: new { formName = "ContactUs" })
    </footer>

    <!-- ... -->
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We call our Child Action, passing the name of the specific Form we want to display, in this case "ContactUs".

Define the Controller

Now we need a Controller with an Action Method to handle this render call:

public class FormController : Controller
{
    private readonly IFormProvider formProvider;
    private readonly IFormComponentVisibilityEvaluator visibilityEvaluator;

    public FormController(
      IFormProvider formProvider,
      IFormComponentVisibilityEvaluator visibilityEvaluator
    {
        this.formProvider = formProvider;
        this.visibilityEvaluator = visibilityEvaluator;
    }

    [ChildActionOnly]
    public ActionResult Form(string formName) => 
        PartialView(CreateFormModel(formName);

    private FormWidgetViewModel CreateFormModel(string formName)
    {
        // ...
    } 
}
Enter fullscreen mode Exit fullscreen mode

We create our view model, which is an instance of Kentico's FormWidgetViewModel. We are using this model because are going to reuse pieces from the View Kentico uses to render Forms as Widgets 🤓.

The code in CreateFormModel can be sourced directly from Lee Conlin's post 🧐, but I'll reproduce it here for convenience:

var formInfo = BizFormInfoProvider
    .GetBizFormInfo(formName, SiteContext.CurrentSiteName);

string className = DataClassInfoProvider
    .GetClassName(formInfo.FormClassID);

var existingBizFormItem = className is null
    ? null
    : BizFormItemProvider
        .GetItems(className)?.GetExistingItemForContact(
           formInfo, contactContext.ContactGuid);

var formComponents = formProvider
    .GetFormComponents(formInfo)
    .GetDisplayedComponents(
      ContactManagementContext.CurrentContact, 
      formInfo, existingBizFormItem, visibilityEvaluator);

var settings = new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    TypeNameHandling = TypeNameHandling.Auto,
    StringEscapeHandling = StringEscapeHandling.EscapeHtml
};

var formConfiguration = JsonConvert.DeserializeObject<FormBuilderConfiguration>(
    formInfo.FormBuilderLayout, settings);

var prefix = Guid.NewGuid().ToString();

// Thanks to Dave in the comments for noticing this assignment is required
ViewData.TemplateInfo.HtmlFieldPrefix = prefix;

return new FormWidgetViewModel
{
    DisplayValidationErrors = true,
    FormComponents = formComponents.ToList(),
    FormConfiguration = formConfiguration,
    FormName = formName,
    FormPrefix = prefix,
    IsFormSubmittable = true,
    SiteForms = new List<SelectListItem>(),
    SubmitButtonImage = formInfo.FormSubmitButtonImage,
    SubmitButtonText = string.IsNullOrEmpty(formInfo.FormSubmitButtonText) 
      ? ResHelper.GetString("general.submit")
      : ResHelper.LocalizeString(formInfo.FormSubmitButtonText)
};
Enter fullscreen mode Exit fullscreen mode

This code is in the FormController for example purposes only. If you use this in your project, add proper error handling and move this logic out of the controller and into something like a request handler 👍🏿.

Use Kentico's View Code

Now that we are able to get all the required parts for rendering a Form Builder form, we can use Kentico's pre-built view code for rendering Form Widgets, which is pretty simple:

<!-- ~/Views/Form/Form.cshtml -->

@using Kentico.Forms.Web.Mvc;
@using Kentico.Forms.Web.Mvc.Widgets;
@using Kentico.Forms.Web.Mvc.Widgets.Internal

@model FormWidgetViewModel

@{
    var config = FormWidgetRenderingConfiguration.Default;

    // @Html.Kentico().FormSubmitButton(Model) requires 
    // this ViewData value to be populated. Normally it
    // executes as part of the Widget rendering, but since
    // we aren't rendering a Widget, we have to do it manually

    ViewData.AddFormWidgetRenderingConfiguration(config);
}

@using (Html.Kentico().BeginForm(Model))
{
    @Html.Kentico().FormFields(Model)

    @Html.Kentico().FormSubmitButton(Model)
}
Enter fullscreen mode Exit fullscreen mode

This code is sourced from Kentico's pre-compiled _FormWidget.cshtml.

This means our statically rendered Form will render the same way as a Form Widget, and submission of the Form will submit to Kentico's existing FormWidgetController - no need to handle any of this ourselves 👏🏽!

Our code can also handle the static rendering of any Form Builder Form 😉, all we need to do is pass a different formName parameter when calling @{ Html.RenderAction("Form", "Form", new { formName = "..." }); } in our views.

Caveats - It's Not A Widget

Because our approach isn't actually rendering a Widget (it isn't running in the context of the Page Builder), we lose out on some functionality...

Specifically, we don't have access to the custom events that get called when a Form Widget's Form Builder form is being rendered 😑.

These events are a powerful way to customize the markup surrounding the Form and its elements and are called by Kentico's internal Widget processing code.

Unfortunately we don't have access to it so we can't even duplicate it in our own code 😒.

Another limitation of not using the Form Widget to render our form is we can't use the Page Builder Form Widget form selector drop down to swap out the form 🤷🏽‍♂️.

Of course, we could add a custom field to a Page Type or define a CMS setting that would hold the formName value of the Form to be rendered. This requires just a little more setup on our end.

Conclusion

Although there are some caveats to using the above approach when rendering Form Builder forms without using Widgets and the Page Builder, the benefits definitely outweigh them 💯.

It's also convenient that we can contain all the functionality for populating the FormWidgetViewModel behind a single Controller Child Action and the view code is very minimal.

I hope you find this little MVC Widget experiment useful - I know it's been helpful for the projects I work on.

I'd also like to thank Lee for providing the building blocks and inspiration for this post 😎.

...

As always, thanks for reading 🙏!


Photo by Scott Graham on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

#kentico

Or my Kentico blog series:

Top comments (2)

Collapse
 
davidconder profile image
David Conder

Hi @seangwright - thanks for this article. It's a common use case to have a form in the footer or somewhere else in the master layout.

Just something to consider, I had an issue because there's one thing missing from Viewdata. If you don't add "ViewData.TemplateInfo.HtmlFieldPrefix", then the form fields don't have a prefix, but the POST does. See slightly amended code below, this worked great for us.

string prefix = Guid.NewGuid().ToString();
        this.ViewData.TemplateInfo.HtmlFieldPrefix = prefix;

        return new FormWidgetViewModel
        {
            DisplayValidationErrors = true,
            FormComponents = formComponents.ToList(),
            FormConfiguration = formConfiguration,
            FormName = formName,
            FormPrefix = prefix,
            IsFormSubmittable = true,
            SiteForms = new List<SelectListItem>(),
            SubmitButtonImage = formInfo.FormSubmitButtonImage,
            SubmitButtonText = string.IsNullOrEmpty(formInfo.FormSubmitButtonText)
              ? ResHelper.GetString("general.submit")
              : ResHelper.LocalizeString(formInfo.FormSubmitButtonText)
        };
Collapse
 
seangwright profile image
Sean G. Wright

Awesome! Thanks for the feedback - I'm not sure if I had that and did a bad copy paste or if I had some different setup that didn't need it?

Anyway, thanks again - I added your addition to the code snippet in the post.