Hi,
We're in a middle of strategic project to our company.
As part of this project we're using Umbraco Forms and with the multi-page form options.
However it's mandatory for us to save the data between pages so the user can return back to see the data and continue the process. In addition the marketing team need to know if users got stuck in the process in between.
I've searched for a solution for this (via coding) however until now I didn't found concrete guide on how to implement it in Umbraco Forms.
I saw other packages might support it (Umbraco Contour or FormEditor) but I really don't want to go back to old versions and loose Umbraco Forms benefits.
In addition I purchased it and seems to me like basic functionality to be able to save partial data in between pages.
Appreciate your help here since our project got stuck due to this issue.
Yes, it is still possible to partially save forms, but it does take a few steps to configure it:
Go to App_Plugins\UmbracoForms\UmbracoForms.config and change the AllowEditableFormSubmissions setting to true.
In an ApplicationEventHandler class, do the following during the ApplicationStarted event:
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
UmbracoFormsController.FormPrePopulate += (object sender, FormEventArgs e) =>
{
// nothing needed here, it just needs to exist to save all form data when submitting a form with multiple form pages
};
}
The records saved will be in the Partially Submitted state when a user moves between pages, until the user hits the final page and clicks the Submit button.
That should work for the current user's session.
If you wish to load a partially submitted form in a future session, you probably need to set a cookie value with the record ID and then pass that into the macro or action you use to render the form on the page.
I only found out about these steps by decompiling the Forms DLLs and checking how the code works there (and some trial and error), so I don't think it's an officially supported feature :)
[Edit] One more thing: the partially submitted entries won't show up in the Forms entries dashboard in the CMS, unless you untick the Approved and Submitted filters at the top of the list.
Thanks again, this time i am monitoring 2 tables to see which one changes when i click next on a multi-form. The two tables are [UFRecordFields], [UFRecords] - where i THINK the partial record would show.
Unfortunately it doesnt update any table where i think the data would be stored for an unfinished form (I get to page 2 before i check the tables). I have made the one change
AllowEditableFormSubmissions = "true"
All other settings are the default, no console errors and i didnt add the method for ApplicationStarted.
The only time a record is created is when i submit the form and its stored in the [UFRecords] table.
So in order to also save the partially submitted record in the database when the page gets changed you need to jump through a few more hoops.
First of all, create a new SurfaceController that inherits from the default UmbracoFormsController:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Security;
using Umbraco.Core.Configuration;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Common;
using Umbraco.Forms.Core.Enums;
using Umbraco.Forms.Data.Storage;
using Umbraco.Forms.Mvc.Attributes;
using Umbraco.Forms.Mvc.BusinessLogic;
using Umbraco.Forms.Mvc.Models;
using Umbraco.Forms.Web.Controllers;
using Umbraco.Forms.Web.Services;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using UmbracoWeb = Umbraco.Web;
namespace MyProject.Controllers.Surface
{
public class FormsController : UmbracoFormsController
{
/// <summary>
/// Handles form submission for saves and submits.
/// </summary>
/// <param name="model"></param>
/// <param name="captchaIsValid"></param>
/// <returns></returns>
[ValidateCaptcha]
[ValidateFormsAntiForgeryToken]
[HttpPost]
[ValidateInput(false)]
public ActionResult HandleFormSubmission(FormViewModel model, bool captchaIsValid)
{
if (Request["__prev"] != null || Request["next"] != null)
{
// save the current form as partially submitted
// this calls a bunch of private methods from the base controller
var form = BaseGetForm(model.FormId);
model.Build(form);
model.FormState = BaseExtractAllPagesState(model, ControllerContext, form);
BaseStoreFormState(model.FormState, model);
BaseResumeFormState(model, model.FormState, false);
SaveForm(form, model, model.FormState, ControllerContext);
TempData[$"FormSaved.{model.FormId}"] = "true";
// redirect back to current page
return RedirectToCurrentUmbracoPage();
}
else
{
// submit form like normal
var result = HandleForm(model, captchaIsValid);
return result;
}
}
/// <summary>
/// Saves the form entry as partially submitted.
/// </summary>
/// <param name="form"></param>
/// <param name="model"></param>
/// <param name="state"></param>
/// <param name="context"></param>
private void SaveForm(Form form, FormViewModel model, Dictionary<string, object[]> state, ControllerContext context)
{
// this method has been copied from the base controller's SubmitForm method and modified for the state
using (ApplicationContext.ProfilingLogger.DebugDuration<UmbracoFormsController>(string.Format("Umbraco Forms: Submitting Form '{0}' with id '{1}'", (object)form.Name, (object)form.Id)))
{
model.SubmitHandled = true;
Record record = new Record();
if (model.RecordId != Guid.Empty)
record = BaseGetRecord(model.RecordId, form);
record.Form = form.Id;
record.State = FormState.PartiallySubmitted;
record.UmbracoPageId = CurrentPage.Id;
record.IP = HttpContext.Request.UserHostAddress;
if (HttpContext.User != null && HttpContext.User.Identity.IsAuthenticated && Membership.GetUser() != null)
record.MemberKey = Membership.GetUser().ProviderUserKey.ToString();
foreach (Field allField in form.AllFields)
{
object[] objArray = new object[0];
if (state != null && state.ContainsKey(allField.Id.ToString()))
objArray = state[allField.Id.ToString()];
object[] array = allField.FieldType.ConvertToRecord(allField, objArray, context.HttpContext).ToArray();
if (record.RecordFields.ContainsKey(allField.Id))
{
record.RecordFields[allField.Id].Values.Clear();
record.RecordFields[allField.Id].Values.AddRange(array);
}
else
{
RecordField recordField = new RecordField(allField);
recordField.Values.AddRange(array);
record.RecordFields.Add(allField.Id, recordField);
}
}
record.RecordData = record.GenerateRecordDataAsJson();
BaseClearFormState(model);
using (var rs = new RecordStorage())
{
if (record.Id <= 0)
{
rs.InsertRecord(record, form);
}
else
{
rs.UpdateRecord(record, form);
}
}
RecordService.Instance.AddRecordIdToTempData(record, ControllerContext);
}
}
#region Base class reflection methods
private MethodInfo getFormMethod;
private Form BaseGetForm(Guid formId)
{
if (getFormMethod == null)
{
getFormMethod = typeof(UmbracoFormsController).GetMethod("GetForm", BindingFlags.NonPublic | BindingFlags.Instance);
}
var obj = getFormMethod.Invoke(this, new object[] { formId });
return obj as Form;
}
private MethodInfo prepopulateFormMethod;
private void BasePrepopulateForm(Form form, ControllerContext context, FormViewModel formViewModel, Record record = null)
{
if (prepopulateFormMethod == null)
{
prepopulateFormMethod = typeof(UmbracoFormsController).GetMethod("PrepopulateForm", BindingFlags.NonPublic | BindingFlags.Instance);
}
prepopulateFormMethod.Invoke(this, new object[] { form, context, formViewModel, record });
}
private MethodInfo extractAllPagesStateMethod;
private Dictionary<string, object[]> BaseExtractAllPagesState(FormViewModel model, ControllerContext context, Form form)
{
if (extractAllPagesStateMethod == null)
{
extractAllPagesStateMethod = typeof(UmbracoFormsController).GetMethod("ExtractAllPagesState", BindingFlags.NonPublic | BindingFlags.Instance);
}
var obj = extractAllPagesStateMethod.Invoke(this, new object[] { model, context, form });
return obj as Dictionary<string, object[]>;
}
private MethodInfo storeFormStateMethod;
private void BaseStoreFormState(Dictionary<string, object[]> state, FormViewModel model)
{
if (storeFormStateMethod == null)
{
storeFormStateMethod = typeof(UmbracoFormsController).GetMethod("StoreFormState", BindingFlags.NonPublic | BindingFlags.Instance);
}
storeFormStateMethod.Invoke(this, new object[] { state, model });
}
private MethodInfo storeResumeFormStateMethod;
private void BaseResumeFormState(FormViewModel model, Dictionary<string, object[]> state, bool editSubmission = false)
{
if (storeResumeFormStateMethod == null)
{
storeResumeFormStateMethod = typeof(UmbracoFormsController).GetMethod("ResumeFormState", BindingFlags.NonPublic | BindingFlags.Instance);
}
storeResumeFormStateMethod.Invoke(this, new object[] { model, state, editSubmission });
}
private MethodInfo getRecordMethod;
private Record BaseGetRecord(Guid recordId, Form form)
{
if (getRecordMethod == null)
{
getRecordMethod = typeof(UmbracoFormsController).GetMethod("GetRecord", BindingFlags.NonPublic | BindingFlags.Instance);
}
var obj = getRecordMethod.Invoke(this, new object[] { recordId, form });
return obj as Record;
}
private MethodInfo clearFormStateMethod;
private void BaseClearFormState(FormViewModel model)
{
if (clearFormStateMethod == null)
{
clearFormStateMethod = typeof(UmbracoFormsController).GetMethod("ClearFormState", BindingFlags.NonPublic | BindingFlags.Instance);
}
clearFormStateMethod.Invoke(this, new object[] { model });
}
#endregion
}
}
As you can see it does a check to see if the Previous or Next page buttons have been pressed and in that case calls the SaveForm method which basically mimics the SubmitForm method, but with a different record state.
I had to add a few reflection methods in there as well, as some of the necessary methods are not public.
To use this controller, you need to update the Render.cshtml view (this might be the Form.cshtml view for older Forms versions) as well:
Note that the above code is for Forms v7.x, so you might have to change it to work with Forms v4 (a decompiler like dnSpy is useful to find out how that version works).
Thanks Tom i will give this a whirl. I cant mark this as an answer but if a mod is reading then i'm happy for this to be marked as an answer and can open a new thread with any other issues.
I am using v7.0.4. And after decompiling Umbraco.Forms.Web.dll it appears it does have ExtractAllPagesState. I'll give your above code a go and see what comes out of it.
Thnaks @Tom it seems to be saving data fine in database from first page, however, when the next button is clicked it keeps redirecting back to the same page, is it because of this return RedirectToCurrentUmbracoPage(); Action Result?
Should I change it to return RedirectToUmbracoPage() and pass value in there?
But then problem is I am not able to find this method get_GoToPageOnSubmit()
on RedirectToUmbracoPage(form.get_GoToPageOnSubmit());
Not sure, but it would be redirecting to the same Umbraco page using RedirectToCurrentUmbracoPage as it will be displaying the same page, it's only the form page that should be changed when the page reloads again.
I can't remember how that is done exactly, so it might be worth looking at the decompiled Forms code to see how that works in your version.
I have managed to nail it down, had to make some more changes to @Tom's surface controller to get the partial submission work as per my requirements.
Had to cover edge cases
Happy to post here if someone needs in the future.
Im applying the custom FormsController, however when I go to the next page and then return it loses the values. It also doesn't seem to be storing them in the DB
Ive literally just c&p the controller Tom put up (seems to have no issues as Im using v7.2), it hits the controller, and saves in the DB, but only saves the guids for the questions, it isn't storing the actual values, and on top of that its not retaining them when I go back a page.
Save the recordId in a Session variable at the end of SaveForm method like below:
RecordService.Instance.AddRecordIdToTempData(record, ControllerContext);
if (Session["Forms_RecordID"] == null)
Session["Forms_RecordID"] = record.UniqueId.ToString();
You then need to implement ForwardNext method like this:
protected void ForwardNext(Form form, FormViewModel model, Dictionary<string, object[]> state)
{
FormViewModel formStep = model;
formStep.FormStep = formStep.FormStep + 1;
SaveForm(form, model, model.FormState, ControllerContext);
if (model.FormStep == form.Pages.Count<Page>()) //If it is the last page
{
model.SubmitHandled = true; //Make as submithandled so it gets redirected to form page for submit message
Session["Forms_RecordID"] = null;
}
For my requirements I didn't require previous button but you can implement that
protected void BackwardPrevious(Form form, FormViewModel model, Dictionary<string, object[]> state)
{
//Put your code here for previous data to persist
}
}
Edit: Forgot to mention in Save form add below after Record record = new Record();
if (Session["Forms_RecordID"] != null)
record = BaseGetRecord(new Guid(Session["Forms_RecordID"].ToString()), form);
This will check if the record is existing, it will pull form values before continuing with next page.
Thats great, does the ForwardNext method need to be called from the HandleFromSubmission or is it something which should be being called automatically (as it doesn't seem to be). For example
One other issue Ive found is that Request["next"] is always null, Ive tried Request["next"] (as 'next ' is the actual id of the button), but it still returns null which is odd
Nope still havnt managed to get it working. Ive downgraded as well.
This is what Im seeing, in the image it shows the first three Request values I looked for in the immediate window (after it had hit the breakpoint from clicking the next button), as you can see all teh values are null. I then ran it through and clicked the previous button, which is then populated in teh request. So frustrating.!
If u want default operation to save data upon every next button press, u don't need this condition. Just check for prev is not null and implement forwardnext and backwardprevious methods as I described above.
Still having issues with this, I dont get the Request ['next'] value at all, Ive tried installing every version from v7 up to 7.2 and its not there. I cant get the code here working to save the form because of this
Why is this not part of the core product like it was back in Contour with the partially submitted workflow? This is standard functionality and it really causing problems with us now.!
i couldnt get the forward and back working too. so i deferred it to the main form handler. the main handler will do the logic to move on/back and hold state.
So i've just SaveForm() (as partially submitted) and then HandleForm(model).
im using forms 8.2 on umbraco 8.3 though.
You can download Telerik JustDecompile and it will help read the Umbraco.Forms.X.dll to help you trace what actually is going on.
Multi-page Form - save data
Hi, We're in a middle of strategic project to our company. As part of this project we're using Umbraco Forms and with the multi-page form options. However it's mandatory for us to save the data between pages so the user can return back to see the data and continue the process. In addition the marketing team need to know if users got stuck in the process in between.
I've searched for a solution for this (via coding) however until now I didn't found concrete guide on how to implement it in Umbraco Forms. I saw other packages might support it (Umbraco Contour or FormEditor) but I really don't want to go back to old versions and loose Umbraco Forms benefits. In addition I purchased it and seems to me like basic functionality to be able to save partial data in between pages.
Appreciate your help here since our project got stuck due to this issue.
Thank you in advance, Moshe
Did you find a solution to this?
Would be great to know if anyone found a solution to this?
Yes, it is still possible to partially save forms, but it does take a few steps to configure it:
Go to
App_Plugins\UmbracoForms\UmbracoForms.config
and change theAllowEditableFormSubmissions
setting totrue
.In an
ApplicationEventHandler
class, do the following during theApplicationStarted
event:The records saved will be in the
Partially Submitted
state when a user moves between pages, until the user hits the final page and clicks the Submit button.That should work for the current user's session. If you wish to load a partially submitted form in a future session, you probably need to set a cookie value with the record ID and then pass that into the macro or action you use to render the form on the page.
I only found out about these steps by decompiling the Forms DLLs and checking how the code works there (and some trial and error), so I don't think it's an officially supported feature :)
[Edit] One more thing: the partially submitted entries won't show up in the Forms entries dashboard in the CMS, unless you untick the
Approved
andSubmitted
filters at the top of the list.Documentation on the settings can be found here:
https://our.umbraco.com/Documentation/Add-ons/UmbracoForms/Developer/Configuration/
Thanks Tom, Im using Umbraco 7.5 with Forms 4.4.7.
I dont seem to have the event FormPrePopulate
Tried using
which threw the error
'Umbraco.Forms.Web.Controllers.UmbracoFormsController' does not contain a definition for 'FormPrePopulate'
Would you know which class i need to add/target?
Thanks again for your input
I was using Umbraco Forms v7.x and I just had a look at the DLL for 4.4.7 and the
FormPrePopulate
method is not there, so it must've been added later.From what I can see from the code in that version, it should work with just the
AllowEditableFormSubmissions
setting enabled.Thanks again, this time i am monitoring 2 tables to see which one changes when i click next on a multi-form. The two tables are [UFRecordFields], [UFRecords] - where i THINK the partial record would show.
Unfortunately it doesnt update any table where i think the data would be stored for an unfinished form (I get to page 2 before i check the tables). I have made the one change
All other settings are the default, no console errors and i didnt add the method for ApplicationStarted.
The only time a record is created is when i submit the form and its stored in the [UFRecords] table.
Any other thoughts?
So in order to also save the partially submitted record in the database when the page gets changed you need to jump through a few more hoops.
First of all, create a new
SurfaceController
that inherits from the defaultUmbracoFormsController
:As you can see it does a check to see if the Previous or Next page buttons have been pressed and in that case calls the
SaveForm
method which basically mimics theSubmitForm
method, but with a different record state.I had to add a few reflection methods in there as well, as some of the necessary methods are not public.
To use this controller, you need to update the
Render.cshtml
view (this might be theForm.cshtml
view for older Forms versions) as well:Note that the above code is for Forms v7.x, so you might have to change it to work with Forms v4 (a decompiler like dnSpy is useful to find out how that version works).
Thanks Tom i will give this a whirl. I cant mark this as an answer but if a mod is reading then i'm happy for this to be marked as an answer and can open a new thread with any other issues.
@Tom - many thanks for this, I can confirm this works great - I only had one issue and that was
ExtractAllPagesState
This didn't exist in 7.03.0 version of forms. I upgraded to the latest and it worked ok after that.
I saved the recordId/UniqueId by changing
SaveForm
slightly to output the new ID so I could save it somewhere to then use again to restore the data..
Thanks again
That looks really helpful. What version of Umbraco Forms did you try this on please?
thanks
It has to be v7.x, but according to Jamie some of the earlier versions (v7.0.x) don't have all the code, so you might have to use at least v7.1.0
Thanks Tom for your quick reply,
I am using v7.0.4. And after decompiling Umbraco.Forms.Web.dll it appears it does have ExtractAllPagesState. I'll give your above code a go and see what comes out of it.
Thnaks @Tom it seems to be saving data fine in database from first page, however, when the next button is clicked it keeps redirecting back to the same page, is it because of this
return RedirectToCurrentUmbracoPage();
Action Result?Should I change it to return
RedirectToUmbracoPage()
and pass value in there?But then problem is I am not able to find this method
get_GoToPageOnSubmit()
onRedirectToUmbracoPage(form.get_GoToPageOnSubmit());
Not sure, but it would be redirecting to the same Umbraco page using
RedirectToCurrentUmbracoPage
as it will be displaying the same page, it's only the form page that should be changed when the page reloads again.I can't remember how that is done exactly, so it might be worth looking at the decompiled Forms code to see how that works in your version.
I have managed to nail it down, had to make some more changes to @Tom's surface controller to get the partial submission work as per my requirements. Had to cover edge cases
Happy to post here if someone needs in the future.
Currently tested on on 7.1.x
Going to test it on 7.0.4
Thanks for your help
I'm working this into my current project, would you be able to post up the final code? thank you
Im applying the custom FormsController, however when I go to the next page and then return it loses the values. It also doesn't seem to be storing them in the DB
Hi Tony,
Post your controller code here mate. Let's see what's going on.
Ive literally just c&p the controller Tom put up (seems to have no issues as Im using v7.2), it hits the controller, and saves in the DB, but only saves the guids for the questions, it isn't storing the actual values, and on top of that its not retaining them when I go back a page.
Save the recordId in a Session variable at the end of
SaveForm
method like below:You then need to implement
ForwardNext
method like this:For my requirements I didn't require previous button but you can implement that
Edit: Forgot to mention in
Save form
add below afterRecord record = new Record();
This will check if the record is existing, it will pull form values before continuing with next page.
Hope that helps
Thats great, does the ForwardNext method need to be called from the HandleFromSubmission or is it something which should be being called automatically (as it doesn't seem to be). For example
Call from
HandleFormSubmission
method :Sorry ash, Ive come back to this after stepping away for a bit. Whereabouts in the handleformsubmit does this go?
It's just that the forward next method saves the form and marks the submithandled to true, but doesn't actually call the HandleForm method?
Do you have your code for that method?
One other issue Ive found is that Request["next"] is always null, Ive tried Request["next"] (as 'next ' is the actual id of the button), but it still returns null which is odd
If u open chrome dev tools and look at the request and see if this 'next' is present in the request.
Unfortunately not, when I dive into the request in Visual studio, its there for the _previous when I go back but not when I move forward with _next.
Have you found solution to this yet on 7.2?
If not ,
Downgrade to 7.1.x you should be able use "next"
Nope still havnt managed to get it working. Ive downgraded as well.
This is what Im seeing, in the image it shows the first three Request values I looked for in the immediate window (after it had hit the breakpoint from clicking the next button), as you can see all teh values are null. I then ran it through and clicked the previous button, which is then populated in teh request. So frustrating.!
If u want default operation to save data upon every next button press, u don't need this condition. Just check for prev is not null and implement forwardnext and backwardprevious methods as I described above.
however if u install 7.1.3 version of forms u should get Request["next"]
@ash how did you resolve the redirect?
im trying this on umbraco 8.2. things are saving nicely to the database, but not redirecting to the right page/step.
Check my posts above with code and implement forward next methods
Still having issues with this, I dont get the Request ['next'] value at all, Ive tried installing every version from v7 up to 7.2 and its not there. I cant get the code here working to save the form because of this
Why is this not part of the core product like it was back in Contour with the partially submitted workflow? This is standard functionality and it really causing problems with us now.!
i couldnt get the forward and back working too. so i deferred it to the main form handler. the main handler will do the logic to move on/back and hold state.
So i've just SaveForm() (as partially submitted) and then HandleForm(model).
im using forms 8.2 on umbraco 8.3 though.
You can download Telerik JustDecompile and it will help read the Umbraco.Forms.X.dll to help you trace what actually is going on.
is working on a reply...
This forum is in read-only mode while we transition to the new forum.
You can continue this topic on the new forum by tapping the "Continue discussion" link below.