Episerver 7 : Creating a custom form block that posts back to a page controller

I have spent many an hour of my life messing around with trying to get information to be passed between different controllers in different states.  A fairly common requirements is having a page that is made up of a form built up via several blocks, for example a three step checkout process, entering in your information, billing and payment details.

Routing

First off, we need to configure our solution to work with Episerver routing, without this we won’t be able to reach our Save action.  The Save action will act as a separate end point from the page default index method.

To expose the Save action, we need to ensure that there is a route registered to our controller action in the applications route tables.  The route table is the way you configure your application so it knows when a particular route/url is made, which controller it should use.  Episerver routing works slightly differently than normal MVC routing, although the underline framework is exactly the same.  To configure your application simply register a generic route as seen below.

public class Global : EPiServer.Global
{
protected override void RegisterRoutes(RouteCollection routes)
{
base.RegisterRoutes(routes);
routes.MapRoute(
"epiRoute",
"Blocks/{controller}/{action}",
new { action = "Index" });
RouteTable.Routes.MapRoute("defaultRoute", "{controller}/{action}");
}
}

Writing Our Form Code

First, in our page we need to create a Form page in order for a content editor to add the functionailty to the site, this will be a simple page with one Content Area.

[EPiServer.DataAnnotations.ContentType(
DisplayName = "Form Page",
GroupName = "Form Page",
Description = "Form Page",
GUID = "A6A309A8-9A61-4F01-BFF6-D99E1AAA3A2A")]
public class FormPage : PageData
{
[Display(
Name = "Contenet Area",
GroupName = SystemTabNames.Content,
Order = 10)]
public virtual ContentArea MainContentArea { get; set; }
}

In order to display this page, we will need a controller.  In the controller, we will need to add a Save action method.  This method will be called when a user clicks the form submission button.

public class FormPageController : PageController<FormPage>
{
public ActionResult Index(FormPage currentPage)
{
return View("Index", currentPage);
}
public ActionResult Save(FormPage currentPage, ShippingAddress address)
{
if (currentPage.MainContentArea != null || currentPage.MainContentArea.Items.Any())
{
var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
foreach (var item in currentPage.MainContentArea.Items)
{
var shippingBlock = contentLoader.Get<ShippingAddressBlock>(item.ContentLink);
if (shippingBlock != null)
{
shippingBlock.Address = address;
}
}
}
return null;
}
}

One big thing to note is that when you post a form to a controller in Episerver, the default behaviour might not work as you might imagine.  If you have a page type with a custom object of address with an Ignore attribute, that gets bound to the form.  When you submit the form and get the current page or block back, the Episerver current page object will not be updated.  Instead you have to use normal MVC model binding ad pass in the extra object as a parameter of your action method. We will also need a view for our page, with the HTML form.

@using (Html.BeginForm("Save", "FormPage", FormMethod.Post))
{
<div role="main" id="main" class="main row">
<div class="large-8 column">
@Html.PropertyFor(x => x.MainContentArea)
</div>
</div>
<button type="submit">Submit</button>
}

So, now we have an Episerver page, a controller and a view that contains a button that will route back to a Save method.  The next thing we need is a ShippingAddressBlock to store the users input and to set-up the default properties on the form, like label and place holder text.

Before we create our shipping block, we need to create a POCO to store our form data in.  This will be done using custom entity called Address, built up like the following:

public class ShippingAddress
{
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string Town { get; set; }
public string Postcode { get; set; }
}

Next we create the block with the forms labels and other properties we are interested in

[ContentType(DisplayName = "Shipping Address Block",
Description = "Shipping Address Block",
GUID = "EC61EA99-0018-4034-942C-ED341B0CD625")]
public class ShippingAddressBlock : BlockData
{
public ShippingAddressBlock()
{
Address = new ShippingAddress();
}
[Ignore]
public ShippingAddress Address { get; set; }
[Display(
Name = "Heading",
GroupName = SystemTabNames.Content,
Order = 100)]
[Required]
public virtual string Heading { get; set; }
[Display(
Name = "Address Line 1 Label",
GroupName = SystemTabNames.Content,
Order = 200)]
[Required]
public virtual string Address1Text { get; set; }
[Display(
Name = "Address Line 2 Label",
GroupName = SystemTabNames.Content,
Order = 300)]
[Required]
public virtual string Address2Text { get; set; }
[Display(
Name = "Town Label",
GroupName = SystemTabNames.Content,
Order = 400)]
[Required]
public virtual string TownText { get; set; }
[Display(
Name = "Postcode Label",
GroupName = SystemTabNames.Content,
Order = 500)]
[Required]
public virtual string PostcodeText { get; set; }

Next we will create the views

@model CustomFormWithRouting.Models.Blocks.ShippingAddressBlock
<h1>@Model.Heading</h1>
<p>
@Html.Label(@Model.Address1Text)
<input type="text" name="AddressLine1" value="@Model.Address.AddressLine1">
</p>
<p>
@Html.Label(@Model.Address2Text)
<input type="text" name="AddressLine2" value="@Model.Address.AddressLine2">
</p>
<p>
@Html.Label(@Model.TownText)
<input type="text" name="Town" value="@Model.Address.Town">
</p>
<p>
@Html.Label(@Model.PostcodeText)
<input type="text" name="Postcode" value="@Model.Address.Postcode">
</p>

Our final solution

Above is an overview of all files in the final solution. Please note that argument and service call validations have been omitted to keep the code as simple as possible.  You can also find all of this code on my GitHub account at the link here 🙂

Tip: The demo site has multi-languages applied, to access the form page goto:

http://localhost:6597/en/test-page/

Jon D Jones

Software Architect, Programmer and Technologist Jon Jones is founder and CEO of London-based tech firm Digital Prompt. He has been working in the field for nearly a decade, specializing in new technologies and technical solution research in the web business. A passionate blogger by heart , speaker & consultant from England.. always on the hunt for the next challenge

More Posts

5 replies
    • Magnush298
      Magnush298 says:

      yes, I’m dowload code from github

      public class ShippingAddress
      {
      public string AddressLine1 { get; set; }

      public ActionResult Save(FormPage currentPage, ShippingAddress address)

      <input type="text" name="Address1Text” value=”@Model.Address.AddressLine1″>

      Reply
      • Magnush298
        Magnush298 says:

        This was my attempt to point out the source of the error:
        public ActionResult Save(FormPage currentPage, ShippingAddress address)
        => the address is always empty

        ps sorry, my english is really bad 🙁

        Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

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