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.
Kentico EMS: MVC Widget Experiments Part 2 - Where Can Widgets Be Used?
Sean G. Wright γ» Mar 30 '20
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>
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>
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)
{
// ...
}
}
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)
};
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)
}
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 π!
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:
Or my Kentico blog series:
Top comments (2)
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.
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.