.Net Core MVC service to implement GDS style multi-page forms
The NHS Design System (and GDS) suggest that complex transactional journeys (see Transactional journeys in this link) should be broken down into steps with one (or sometimes two related) pieces of information being asked of the user on each page.
At the end of the journey, we should show a check your answers page summarising user responses before they submit them (usually to the database) with links to go back and change our answers.
To implement this in .Net, we need to retain the user's responses across the multi-page form.
The multi-page form service provides a mechanism for doing this.
The most obvious place to store users' responses before they are committed to the database is in TempData. TempData uses browser cookies to store data, though, and when we are storing a complex data model (lots of questions/options across multiple pages) the cookie can quickly become too large, causing the browser to crash with a “Bad request” error.
The multi-page form service uses the application database to store the transactional data as a JSON string against a GUID so that it can be created, updated and retrieved throughout the transactional journey.
The table used to store the temp data
The NuGet package will create this table if it doesn’t already exist.
When a user gets to the end of the transactional journey and submits their choices, the associated MultiPageFormData record is deleted.
A job or function should be set up to delete MultiPageFormData records (for example that are more than 7 days old) to ensure records relating to transactions journeys that are not completed are tidied up.
A controller method should be created to start the multi-page transactional flow. This should:
- Clear TempData
- Invoke the multi-page form service, passing it the model for the data being captured
- Redirect to an action that will return the view for the first page of the transaction
[HttpGet("AddCourseNew")]
public IActionResult AddCourseNew()
{
//1. Clear Tempdata:
TempData.Clear();
//2. Invoke the multi-page form service, passing it the model for the data being captured:
multiPageFormService.SetMultiPageFormData(
new AddNewCentreCourseTempData(),
MultiPageFormDataFeature.AddNewCourse,
TempData
);
//3. Redirect to an action that will return the view for the first page of the transaction:
return RedirectToAction("SelectCourse");
}
Each step of the flow will have a controller get method which should:
- Use the multipage form service to retrieve transactional data
- Populate the view model with user''s selections from the data
- Return the view
[HttpGet("AddCourse/SelectCourse")]
public IActionResult SelectCourse(
string? categoryFilterString = null,
string? topicFilterString = null
)
{
//1. Use the multipage form service to retrieve transactional data:
var data = multiPageFormService.GetMultiPageFormData<AddNewCentreCourseTempData>(
MultiPageFormDataFeature.AddNewCourse,
TempData
);
//2. Populate the view model with users selections from the data:
var model = GetSelectCourseViewModel(
categoryFilterString ?? data.CategoryFilter,
topicFilterString ?? data.TopicFilter,
data.Application?.ApplicationId
);
//3. Return the view:
return View("AddNewCentreCourse/SelectCourse", model);
}
On submit their selection for each step of the form, the POST method should:
- Use the multipage form service to retrieve transactional data
- Update the data with the selections submitted by the user
- Store the updated data using the multipage form service
- Redirect to the GET action method for the next step in the transaction
[HttpPost("AddCourse/SelectCourse")]
public IActionResult SelectCourse(
int? applicationId,
string? categoryFilterString = null,
string? topicFilterString = null
)
{
//1. Use the multipage form service to retrieve transactional data:
var data = multiPageFormService.GetMultiPageFormData<AddNewCentreCourseTempData>(
MultiPageFormDataFeature.AddNewCourse,
TempData
);
if (applicationId == null)
{
ModelState.AddModelError("ApplicationId", "Select a course");
return View(
"AddNewCentreCourse/SelectCourse",
GetSelectCourseViewModel(
categoryFilterString,
topicFilterString
)
);
}
var centreId = User.GetCentreId();
var categoryId = User.GetAdminCourseCategoryFilter();
var selectedApplication =
courseService.GetApplicationOptionsAlphabeticalListForCentre(centreId, categoryId)
.Single(ap => ap.ApplicationId == applicationId);
//2. Update the data with the selections submitted by the user:
data.CategoryFilter = categoryFilterString;
data.TopicFilter = topicFilterString;
data!.SetApplicationAndResetModels(selectedApplication);
//3. Store the updated data using the multipage form service:
multiPageFormService.SetMultiPageFormData(data, MultiPageFormDataFeature.AddNewCourse, TempData);
//4: Redirect to the GET action method for the next step in the transaction:
return RedirectToAction("SetCourseDetails");
}
The GET method for the summary page, should:
- Use the multipage form service to retrieve transactional data
- Populate the view model with user's selections from the data
- Return the view
The view should show all of the selections made by the user with “Change” links allowing the user to return to any step in the transaction. See NHS Design System prototype example.
[HttpGet("AddCourse/Summary")]
public IActionResult Summary()
{
//1. Use the multipage form service to retrieve transactional data:
var data = multiPageFormService.GetMultiPageFormData<AddNewCentreCourseTempData>(
MultiPageFormDataFeature.AddNewCourse,
TempData
);
//2. Populate the view model with user's selections from the data
var model = new SummaryViewModel(data!);
//3. Return the view
return View("AddNewCentreCourse/Summary", model);
}
The POST method for the summary page, triggered by submitting, should:
- Use the multipage form service to retrieve transactional data
- Commit the data to the database (using an update or insert service method or API call)
- Use the multipage form service to remove the transactional data
- Clear TempData
- Redirect to a confirmation screen
[HttpPost("AddCourse/Summary")]
public IActionResult? CreateNewCentreCourse()
{
//1. Use the multipage form service to retrieve transactional data
var data = multiPageFormService.GetMultiPageFormData<AddNewCentreCourseTempData>(
MultiPageFormDataFeature.AddNewCourse,
TempData
);
using var transaction = new TransactionScope();
var customisation = GetCustomisationFromTempData(data!);
//2. Commit the data to the database (using an update or insert service method or API call):
var customisationId = courseService.CreateNewCentreCourse(customisation);
...
//3. Use the multipage form service to remove the transactional data:
multiPageFormService.ClearMultiPageFormData(MultiPageFormDataFeature.AddNewCourse, TempData);
transaction.Complete();
//4. Clear TempData
TempData.Clear();
TempData.Add("customisationId", customisationId);
TempData.Add("applicationName", data.Application!.ApplicationName);
TempData.Add("customisationName", data.CourseDetailsData!.CustomisationName);
//5. Redirect to a confirmation screen
return RedirectToAction("Confirmation");
}
The preceding code samples all rely on predefined names like MultiPageFormDataFeature.AddNewCourse and MultiPageFormDataFeature.AddRegistrationPrompt. In scenarios demanding the use of names not available within the MultiPageFormDataFeature class, we have the option to employ a function instead of the pre-existing names.
We can utilise MultiPageFormDataFeature.AddCustomWebForm("NewCourseCWF") in place of MultiPageFormDataFeature.AddNewCourse. However, It's important to note that any custom feature name must end with CWF
; failure to do so will result in an exception being thrown.
//1. Invoke the multi-page form service, passing it the model for the data being captured:
multiPageFormService.SetMultiPageFormData(
new AddNewCentreCourseTempData(),
MultiPageFormDataFeature.AddCustomWebForm("NewCourseCWF"),
TempData
);
//2. Use the multipage form service to retrieve transactional data:
var data = multiPageFormService.GetMultiPageFormData<AddNewCentreCourseTempData>(
MultiPageFormDataFeature.AddCustomWebForm("NewCourseCWF"),
TempData
);
//3. Use the multipage form service to remove the transactional data:
multiPageFormService.ClearMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("NewCourseCWF"), TempData);