If you are working on a complex website where forms are being programmatically generated, you might run into the issue of configuring form layouts.
Kentico provides a powerful "Form builder" feature, with drag-and-drop functionality for constructing forms. However, this feature does not help much with programmatically generated forms. Setting CSS classes on the settings of each field can help, but it does not allow you to pre-define the HTML structure of the form.
Ultimately, configuring a custom HTML structure of a form layout is only possible if the "automatically generated form layout" setting is changed. This requires you to instruct the client to manually edit form layouts, which is unintuitive (and usually impossible).
Fortunately, the auto-generated form layout functionality can be overriden and customised. The steps are:
AbstractDefaultLayout
BizForm
The classes that generate the layouts inherit the AbstractDefaultLayout
class, which contains methods such as CreateField
, used for rendering field labels and controls.
Due to the complexity of the AbstractDefaultLayout
class, we are better off inheriting the DivDefaultLayout
class that already implements the required methods.
public class CustomLayout : DivDefaultLayout
{
public CustomLayout(BasicForm basicForm, Panel categoryListPanel) : base(basicForm, categoryListPanel)
{
}
}
With our class set up, we can proceed with overriding the CreateField
method.
This method is responsible for constructing the required HTML elements for each form field.
The first thing we should do when overriding the CreateField
method is to make sure we do not need to deal with the form "design mode".
Design mode represents the state of the form while it is being edited in the "Form builder".
As this is not something we need to customise, we can use the behaviour of the base method.
public class CustomLayout : DivDefaultLayout
{
public CustomLayout(BasicForm basicForm, Panel categoryListPanel) : base(basicForm, categoryListPanel)
{
}
protected override void CreateField(FieldCreationArgs args)
{
if (BasicForm.IsDesignMode)
{
base.CreateField(args);
return;
}
}
}
We can now use the AddControlToPanel
method to add the necessary elements to the form. This includes elements that define the HTML structure of our form, as well as control labels and actual form controls.
void AbstractLayout.AddControlToPanel(Control control, IDataDefinitionItem parentControl);
For example, to add a <div>
element:
protected override void CreateField(FieldCreationArgs args)
{
if (BasicForm.IsDesignMode)
{
base.CreateField(args);
return;
}
var panel = new Panel();
panel.CssClass = "my-div";
AddControlToPanel(panel, args.FormFieldInfo);
}
Providing the args.FormFieldInfo
property to the AddControlToPanel
method ensures our <div>
element is added to the HTML structure of the field that is being created. Remember - CreateField
runs for each field in the form.
Another approach to add elements is to add LiteralControl
objects, as it allows for a "flat" approach of constructing the form.
protected override void CreateField(FieldCreationArgs args)
{
if (BasicForm.IsDesignMode)
{
base.CreateField(args);
return;
}
AddControlToPanel(new LiteralControl("<div class='my-div'>"), args.FormFieldInfo);
// More elements here..
AddControlToPanel(new LiteralControl("</div>"), args.FormFieldInfo);
}
Combined with the AddControlToPanel
method, we can use CreateFieldLabel
and CreateEditingFormControl
methods to create labels and form controls.
protected override void CreateField(FieldCreationArgs args)
{
if (BasicForm.IsDesignMode)
{
base.CreateField(args);
return;
}
AddControlToPanel(new LiteralControl("<div class='field'>"), args.FormFieldInfo);
AddControlToPanel(CreateFieldLabel(args.FormFieldInfo), args.FormFieldInfo);
AddControlToPanel(CreateEditingFormControl(args.FormFieldInfo), args.FormFieldInfo);
AddControlToPanel(new LiteralControl("</div>"), args.FormFieldInfo);
}
As the CreateField
method has a FieldCreationArgs
parameter with a FormFieldInfo
property, we can use the GetPropertyValue
method to retrieve the values for the CSS classes defined under the "CSS styles" section of the form field properties.
For example, to get the field CSS class:
var fieldCssClass = args.FormFieldInfo.GetPropertyValue(FormFieldPropertyEnum.FieldCssClass);
We can then integrate this value when creating LiteralControl
objects to construct our markup.
One thing we need to consider is that the CSS class value can be empty.
To accommodate this scenario, we can use the CMS.Helpers.CssHelper.GetCssClassAttribute(string cssClass)
method, which returns a class attribute if the string parameter is not empty - otherwise, an empty string is returned.
protected override void CreateField(FieldCreationArgs args)
{
var fieldCssClass = args.FormFieldInfo.GetPropertyValue(FormFieldPropertyEnum.FieldCssClass);
// . . .
var fieldCssClassAttribute = CssHelper.GetCssClassAttribute(fieldCssClass);
AddControlToPanel(new LiteralControl(string.Format("<div{0}>", fieldCssClassAttribute)), args.FormFieldInfo);
The final step is to create a custom class that inherits the BizForm
class.
We can use it to load our previously created custom layout, by overriding the LoadDefaultLayout
method.
public class CustomBizForm : BizForm
{
protected override void LoadDefaultLayout()
{
// Create a new instance of the custom layout
var layout = new CustomLayout(this, categoryListPanel);
// Load the layout, and assign it as the layout of the form
layout.LoadLayout();
Layout = layout;
}
}
We can reference this class in our custom BizForm webpart.
<%@ Control Language="C#" AutoEventWireup="true" Inherits="CMSWebParts_Custom_CustomBizForm" CodeFile="~/CMSWebParts/Custom/CustomBizForm.ascx.cs" %>
<%@ Register TagPrefix="KenticoTricks" Namespace="KenticoTricks.FormLayouts" Assembly="KenticoTricks.FormLayouts" %>
<KenticoTricks:CustomBizForm ID="viewBiz" runat="server" IsLiveSite="true" />
Let's look at implementing the markup of a Bootstrap horizontal form in a custom layout class.
This would be the markup required for our form:
<div class="form-horizontal">
<div class="form-group">
<label for="input" class="col-sm-2 control-label">(label text)</label>
<div class="col-sm-10">
(input control)
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
(submit button)
</div>
</div>
</div>
Our CreateField
method would then look like this:
protected override void CreateField(FieldCreationArgs args)
{
if (BasicForm.IsDesignMode)
{
base.CreateField(args);
return;
}
if (BasicForm.FieldsToHide.Contains(args.FormFieldInfo.Name))
{
args.FormFieldInfo.Visible = false;
}
var labelCssClass = args.FormFieldInfo.GetPropertyValue(FormFieldPropertyEnum.CaptionCssClass);
// Add form group
var pnl = new Panel { CssClass = "form-group" };
AddControlToPanel(pnl, args.FormFieldInfo);
// Add label
var fieldLabel = CreateFieldLabel(args.FormFieldInfo);
fieldLabel.CssClass = "col-sm-2 control-label " + labelCssClass;
pnl.Controls.Add(fieldLabel);
// Open fields div
pnl.Controls.Add(new LiteralControl("<div class='col-sm-10'>"));
// Add form control
pnl.Controls.Add(CreateEditingFormControl(args.FormFieldInfo));
// Close field div
pnl.Controls.Add(new LiteralControl("</div>"));
}
The submit button is automatically added to the form. By overriding the Load
method, we can define different markup:
protected override void Load()
{
base.Load();
// Get the submit button control
var submitButton = (Control)GetSubmitButton();
// Create a new panel to contain the submit button
var submitButtonPanel = new Panel { CssClass = "form-group" };
// Add wrapper div and submit button to panel
submitButtonPanel.Controls.Add(new LiteralControl("<div class='col-sm-offset-2 col-sm-10'>"));
submitButtonPanel.Controls.Add(submitButton);
submitButtonPanel.Controls.Add(new LiteralControl("</div>"));
// Add the panel to the form
FormPanel.Controls.Add(submitButtonPanel);
}