Since the release of EPiServer 7 pages and blocks can be created from code. Before version 7 most developers used the PageTypeBuilder to do this. There are a couple of advantages for using this functionality such as better code architecture and easier deployments.
Lately I did some research how strongly typed pages and blocks works in EPiServer. After some time I started (still work in progress) to build strongly typed XForms. The idea is to create a class that represents a form. The fields of the form are defined as properties. The class and properties are decorated with attributes to configure the form in XForms. The form will be created or updated while EPiServer is initializing, the same way as how pages and blocks are created.
This solution can be handy if you want to create a form from code, but remember that users should not use the user interface for managing the form. I started this for learning more how strongly typed pages and blocks works in EPiServer. Now let’s get started with the code! You can find the full source code on GitHub.
To create a form during the initialization process I used these steps:
The above steps are only executed while EPiServer is initializing. Now let’s get a closer look at what happens in these steps.
With the assembly attribute TypeScannerRegistration a new plug-in type can be registered. While scanning assemblies plug-in types are loaded into a ITypeScannerLookup type. I’ve created an abstract class called FormData, classes that represents a form should inherit this class. The FormData class is being registered as a plug-in type. To do this the below assembly attribute is defined in the AssemblyInfo class.
[assembly: TypeScannerRegistration(typeof(FormData))]
Now all types that inherit from the FormData class are loaded into the lookup object. The FormTypeModelSynchronizer selects all types that inherits from the FormData class and are not abstract. For each type it creates a FormTypeModel.
public class FormTypeModelSynchronizer : IFormTypeModelSynchronizer
{
private readonly IList<FormTypeModel> _formTypeModels;
private readonly IXFormBuilder _xFormBuilder;
private readonly IFormFieldTypeSynchronizer _formFieldTypeSynchronizer;
private readonly ITypeScannerLookup _typeScannerLookup;
/// <summary>
/// Constructor
/// </summary>
public FormTypeModelSynchronizer(IXFormBuilder xFormBuilder, IFormFieldTypeSynchronizer formFieldTypeSynchronizer, ITypeScannerLookup typeScannerLookup)
{
_formTypeModels = new List<FormTypeModel>();
_xFormBuilder = xFormBuilder;
_formFieldTypeSynchronizer = formFieldTypeSynchronizer;
_typeScannerLookup = typeScannerLookup;
}
/// <summary>
/// Create model
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private FormTypeModel CreateModel(Type type)
{
var formTypeModel = Activator.CreateInstance<FormTypeModel>();
formTypeModel.FormType = type;
object[] attributes = formTypeModel.FormType.GetCustomAttributes(typeof(FormTypeAttribute), false);
var formTypeAttribute = attributes[0] as FormTypeAttribute;
if (formTypeAttribute != null)
{
formTypeModel.Guid = Guid.Parse(formTypeAttribute.GUID);
formTypeModel.Name = formTypeAttribute.Name;
formTypeModel.FormCanBeSentWithoutLoggingIn = formTypeAttribute.FormCanBeSentWithoutLoggingIn;
formTypeModel.SamePersonCanSendTheFormSeveralTimes = formTypeAttribute.SamePersonCanSendTheFormSeveralTimes;
var fields = _formFieldTypeSynchronizer.Analyze(formTypeModel);
foreach (var field in fields)
{
formTypeModel.Fields.Add(field);
}
}
return formTypeModel;
}
/// <summary>
/// Commit the forms
/// </summary>
public void Commit()
{
foreach (var item in _formTypeModels)
{
_xFormBuilder.Create(item).Save();
}
}
/// <summary>
/// Search for FormData implementations
/// </summary>
public void Analyze()
{
var formDataTypes = _typeScannerLookup.AllTypes.Where(
t => typeof(FormData).IsAssignableFrom(t) &&
t != typeof(FormData) &&
!t.IsAbstract);
foreach (var item in formDataTypes)
{
_formTypeModels.Add(CreateModel(item));
}
}
}
The FormFieldTypeSynchronizer reads the properties with reflection from the form class. Only properties that are defined public and contains a getter and setter are used. Also the property should be decorated with an attribute that inherits from the FormFieldAttribute. The code supports the below form fields. All form field attributes inherits from the FormFieldAttribute.
For all property attributes a model that inherits from FormFieldModel is created and added to a list on the FormTypeModel.
public class FormFieldTypeSynchronizer : IFormFieldTypeSynchronizer
{
/// <summary>
/// Analyze for form field properties
/// </summary>
/// <param name="formFieldType"></param>
/// <returns></returns>
public IList<FormRowModel> Analyze(FormTypeModel formFieldType)
{
var rows = new List<FormRowModel>();
var properties = GetProperties(formFieldType);
foreach (var property in properties)
{
var attributes = property.GetCustomAttributes(true);
var formFieldAttribute = attributes.OfType<FormFieldAttribute>().FirstOrDefault();
if (formFieldAttribute != null)
{
var row = new FormRowModel();
var column = new FormColumnModel();
column.FormFields.Add(formFieldAttribute.FromAttribute());
row.Columns.Add(column);
rows.Add(row);
}
}
return rows;
}
/// <summary>
/// Get properties
/// </summary>
/// <param name="formFieldType"></param>
/// <returns></returns>
private IEnumerable<PropertyInfo> GetProperties(FormTypeModel formFieldType)
{
var propertyInfoArray = formFieldType.FormType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty);
foreach (var propertyInfo in this.FilterPropertiesWithPublicGetAndSetAccessor(propertyInfoArray))
{
if (propertyInfo.IsDefined(typeof(IgnoreAttribute), true))
{
continue;
}
yield return propertyInfo;
}
}
/// <summary>
/// Filter properties with public get and set accessor
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
private IEnumerable<PropertyInfo> FilterPropertiesWithPublicGetAndSetAccessor(IEnumerable<PropertyInfo> properties)
{
return properties.Where(property =>
{
if (property.GetGetMethod() == null)
{
return false;
}
return property.GetSetMethod() != null;
});
}
}
For this example I created a ContactFormData class which inherit from the FormData type. This means that the ContactFormData class is being scanned and saved in a lookup object while EPiServer is starting up. The class attribute FormType is defined on the class, this attribute contains properties that reflect with properties of the FormDefinition type. Properties of the FormDefinition are also configurable in the XForms user interface. A GUID property is defined on the FormType attribute which will prevent that a new form is created each time EPiServer is starting up, this works exactly the same as with pages and blocks.
The ContactFormData class contains several properties that are decorated with a property attribute. Each property is a field on the actual contact form. The property attribute defines how the field should be used in XForms.
[FormType(
GUID = "B6CE7290-D1A5-46FF-94C5-380B44331032",
Name = "Contact form test",
CreatedBy = "Patrick van Kleef",
FormCanBeSentWithoutLoggingIn = true,
SamePersonCanSendTheFormSeveralTimes = true)]
public class ContactFormData : FormData
{
[TextFormFieldType(
Name = "Name",
Heading = "Name",
NumberOfCharacters = 20,
CssClass = "textfield",
ToolTip = "Name",
SortOrder = 1,
Required = true)]
public string NameTextField { get; set; }
[RadioButtonFormFieldType(
Name = "Gender",
Heading = "Gender",
Required = true,
Options = new[] { "Male", "Female" },
HorizontalAlign = true,
SortOrder = 2)]
public string GenderRadioButtonListField { get; set; }
[TextFormFieldType(
Name = "Age",
Heading = "Age",
NumberOfCharacters = 20,
CssClass = "textfield",
ToolTip = "Age",
SortOrder = 3,
ValidateAs = TextFormFieldValidateType.PositiveInteger)]
public string AgeTextField { get; set; }
[TextFormFieldType(
Name = "DateOfBirth",
Heading = "Date of birth",
NumberOfCharacters = 20,
CssClass = "datefield",
ToolTip = "Date of birth",
SortOrder = 4,
ValidateAs = TextFormFieldValidateType.Date_MM_DD_YYYY)]
public string DateOfBirthTextField { get; set; }
[TextFormFieldType(
Name = "EmailAddress",
Heading = "Emailadress",
NumberOfCharacters = 20,
CssClass = "textfield",
ToolTip = "Emailaddress",
SortOrder = 5,
ValidateAs = TextFormFieldValidateType.EmailAddress)]
public string EmailAddressTextField { get; set; }
[TextFormFieldType(
Name = "Company",
Heading = "Company",
NumberOfCharacters = 20,
CssClass = "textfield",
ToolTip = "Company",
SortOrder = 6)]
public string CompanyTextField { get; set; }
[DropdownFormFieldType(
Name = "Country",
Heading = "Country",
Required = true,
Options = new[] { "Netherlands", "Belgium", "Sweden", "Germany" },
SortOrder = 7)]
public string CountryDropdownField { get; set; }
[CheckboxListFormFieldType(
Name = "FavoriteColor",
Heading = "Favorite color",
Required = true,
Options = new[] { "Blue", "Green", "Yellow" },
HorizontalAlign = true,
SortOrder = 8)]
public string FavoritesCheckboxListField { get; set; }
[TextareaFormFieldType(
Name = "Message",
Heading = "Message",
NumberOfCharacters = 20,
NumberOfRows = 6,
ToolTip = "Message",
SortOrder = 9)]
public string MessageTextareaField { get; set; }
[SubmitButtonFormType(
ButtonText = "Send",
Action = SubmitFormActions.SaveToDatabaseAndSendMail,
RecipientEmailAddress = "[email protected]",
SenderEmailAddress = "[email protected]",
EmailSubject = "Contact form",
SortOrder = 10)]
public SubmitFormActions ActionButton { get; set; }
}
I’m still working on this project, you can find the full source code on GitHub. With the current version of the source code you can create you’re own form by using the FormData and attributes as explained above. Next thing I will working on is rendering the form on a MVC view.
Please feel free to give me feedback.