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.
I managed to get this working on Umbraco 7, Forms Version 7.1.3.
I like many others had issues with the code above. What I did was look at Umbraco 8 and it's version of Forms, this worked. Using Telerick JustDecompile I implemented the newer version of the code to my older version of Umbraco.
public class FormsController : UmbracoFormsController
{
private readonly IScopeProvider _scopeProvider;
public string RecordId { get; set; }
[HttpPost]
[ValidateCaptcha]
[ValidateFormsAntiForgeryToken]
[ValidateInput(false)]
public ActionResult HandleFormSubmission(FormViewModel model, bool captchaIsValid)
{
ActionResult umbracoPage;
var form = BaseGetForm(model.FormId);
model.Build(form);
if (!HoneyPotIsEmpty(model))
model.SubmitHandled = true;
else
{
PrePopulateForm(form, ControllerContext, model, null);
model.FormState = FormPrePopulate == null ? ExtractCurrentPageState(model, ControllerContext, form) : ExtractAllPagesState(model, ControllerContext, form);
StoreFormState(model.FormState, model);
OnFormHandled(form, model);
ResumeFormState(model, model.FormState, false);
var prevClicked = (!string.IsNullOrEmpty(Request["__prev"]) || !string.IsNullOrEmpty(Request["PreviousClicked"])) && model.FormStep > 0;
if (prevClicked)
BackwardPrevious(form, model, model.FormState);
else
ForwardNext(form, model, model.FormState);
model.IsFirstPage = model.FormStep == 0;
model.IsLastPage = model.FormStep == form.Pages.Count - 1;
}
OnFormHandled(form, model);
StoreFormModel(model);
if (!model.SubmitHandled ? true : form.GoToPageOnSubmit <= 0)
umbracoPage = CurrentUmbracoPage();
else
{
ClearFormModel();
ClearFormState(model);
umbracoPage = RedirectToUmbracoPage(form.GoToPageOnSubmit);
}
return umbracoPage;
}
protected virtual void OnFormHandled(Form form, FormViewModel model)
{
}
private void ResumeFormState(FormViewModel model, Dictionary<string, object[]> state, bool editSubmission = false)
{
if (state != null)
{
foreach (PageViewModel page in model.Pages)
{
foreach (FieldsetViewModel fieldset in page.Fieldsets)
{
foreach (FieldsetContainerViewModel container in fieldset.Containers)
{
foreach (FieldViewModel field in container.Fields)
{
if (editSubmission && field.FieldType.Id == Guid.Parse("A72C9DF9-3847-47CF-AFB8-B86773FD12CD"))
{
var providerInstance = FieldTypeProviderCollection.Instance.GetProviderInstance(Guid.Parse("DA206CAE-1C52-434E-B21A-4A7C198AF877"));
field.FieldType = providerInstance;
field.HideLabel = true;
}
if (!state.ContainsKey(field.Id))
{
continue;
}
field.Values = state[field.Id];
}
}
}
}
}
}
private static string Base64Encode(string plainText)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(plainText));
}
private void StoreFormState(Dictionary<string, object[]> state, FormViewModel model)
{
string str = JsonConvert.SerializeObject(state);
model.RecordState = Base64Encode(str.EncryptWithMachineKey());
}
private Dictionary<string, object[]> ExtractAllPagesState(FormViewModel model, ControllerContext context, Form form)
{
object[] objArray;
Dictionary<string, object[]> strs = RetrieveFormState(model);
if (strs == null)
{
return null;
}
foreach (Field allField in form.AllFields)
{
object[] objArray1 = new object[0];
object[] objArray2 = form.AllFields.First((Field f) => f.Id == allField.Id).Values == null ? new object[0] : form.AllFields.First((Field f) => f.Id == allField.Id).Values.ToArray();
if (!context.HttpContext.Request.Form.AllKeys.Contains(allField.Id.ToString()))
{
objArray1 = objArray2;
}
else
{
string[] values = context.HttpContext.Request.Form.GetValues(allField.Id.ToString());
bool flag = true;
if (values != null)
{
string[] strArrays = values;
int num = 0;
while (num < (int)strArrays.Length)
{
if (string.IsNullOrEmpty(strArrays[num]))
{
num++;
}
else
{
flag = false;
break;
}
}
if (flag)
{
objArray = objArray2;
}
else
{
objArray = values;
}
objArray1 = objArray;
}
}
if (!strs.ContainsKey(allField.Id.ToString()))
{
strs.Add(allField.Id.ToString(), objArray1);
}
else
{
strs[allField.Id.ToString()] = objArray1;
}
}
return strs;
}
private Dictionary<string, object[]> ExtractCurrentPageState(FormViewModel model, ControllerContext context, Form form)
{
Dictionary<string, object[]> strs = RetrieveFormState(model);
if (strs != null)
{
foreach (FieldsetViewModel fieldset in model.CurrentPage.Fieldsets)
{
foreach (FieldsetContainerViewModel container in fieldset.Containers)
{
foreach (FieldViewModel fieldViewModel in container.Fields)
{
object[] values = new object[0];
if (context.HttpContext.Request.Form.AllKeys.Contains(fieldViewModel.Id))
{
values = context.HttpContext.Request.Form.GetValues(fieldViewModel.Id);
}
Field field1 = form.AllFields.First((Field field) => field.Id.ToString() == fieldViewModel.Id);
values = field1.FieldType.ProcessSubmittedValue(field1, values, context.HttpContext).ToArray();
if (!strs.ContainsKey(fieldViewModel.Id))
{
strs.Add(fieldViewModel.Id, values);
}
else
{
strs[fieldViewModel.Id] = values;
}
}
}
}
}
return strs;
}
private static string Base64Decode(string base64EncodedData)
{
byte[] numArray = Convert.FromBase64String(base64EncodedData);
return Encoding.UTF8.GetString(numArray);
}
private Dictionary<string, object[]> RetrieveFormState(FormViewModel model)
{
if (string.IsNullOrEmpty(model.RecordState))
{
return new Dictionary<string, object[]>();
}
string str = Base64Decode(model.RecordState);
return JsonConvert.DeserializeObject<Dictionary<string, object[]>>(str.DecryptWithMachineKey());
}
private void PrePopulateForm(Form form, ControllerContext context, FormViewModel formViewModel, Record record = null)
{
Dictionary<string, object[]> strs = RetrieveFormState(formViewModel);
object[] value = new object[0];
if (FormPrePopulate != null)
{
foreach (Field allField in form.AllFields)
{
if (!context.HttpContext.Request.Form.AllKeys.Contains(allField.Id.ToString()))
{
KeyValuePair<string, object[]> keyValuePair = strs.FirstOrDefault((KeyValuePair<string, object[]> v) => v.Key == allField.Id.ToString());
value = keyValuePair.Value;
if (value == null)
{
continue;
}
object[] objArray = value;
for (int i = 0; i < (int)objArray.Length; i++)
{
object obj = objArray[i];
if (allField.Values == null)
{
allField.Values = new List<object>();
}
else if (allField.Settings.Keys.Contains("DefaultValue"))
{
allField.Values.Clear();
}
if (obj.Equals(""))
{
allField.Values = null;
}
else
{
allField.Values.Add(obj);
}
}
}
else
{
value = context.HttpContext.Request.Form.GetValues(allField.Id.ToString());
if (allField.Values == null)
{
allField.Values = new List<object>();
}
else if (allField.Settings.Keys.Contains("DefaultValue"))
{
allField.Values.Clear();
}
object[] objArray1 = value;
for (int j = 0; j < (int)objArray1.Length; j++)
{
object obj1 = objArray1[j];
if (obj1.Equals(""))
{
allField.Values = null;
}
else
{
allField.Values.Add(obj1);
}
}
}
if (!strs.ContainsKey(allField.Id.ToString()))
{
strs.Add(allField.Id.ToString(), value);
}
else
{
strs[allField.Id.ToString()] = value;
}
}
bool? nullable = null;
using (IScope scope = _scopeProvider.CreateScope(IsolationLevel.Unspecified, RepositoryCacheMode.Unspecified, null, nullable, false))
{
scope.Events.Dispatch<FormEventArgs>(FormPrePopulate, this, new FormEventArgs(form), "PrePopulatingForm");
DistributedCache.Instance.RefreshFormsCache(form);
DistributedCache.Instance.RefreshAllPrevalueRuntimeCache();
scope.Complete();
}
if (record != null)
{
foreach (Field field in form.AllFields)
{
object[] array = new object[0];
array = field.FieldType.ConvertToRecord(field, array, ControllerContext.HttpContext).ToArray();
if (record.RecordFields.ContainsKey(field.Id))
{
continue;
}
RecordField recordField = new RecordField(field);
recordField.Values.AddRange(array);
record.RecordFields.Add(field.Id, recordField);
}
foreach (KeyValuePair<Guid, RecordField> values in record.RecordFields)
{
foreach (Field allField1 in form.AllFields)
{
if (!(allField1.Id == values.Key) || allField1.Values == null)
{
continue;
}
values.Value.Values = allField1.Values;
}
}
}
}
}
private bool HoneyPotIsEmpty(FormViewModel model)
{
var request = Request;
Guid formId = model.FormId;
return string.IsNullOrEmpty(request[formId.ToString().Replace("-", string.Empty)]);
}
private void ClearFormModel()
{
TempData.Remove("umbracoformsform");
}
private void ClearFormState(FormViewModel model)
{
model.RecordState = string.Empty;
}
private bool IsMemberSignedIn()
{
var memberShipHelper = new MembershipHelper(UmbracoWeb.UmbracoContext.Current);
var userName = memberShipHelper.GetCurrentMember();
if (userName != null) return true;
return false;
}
private void SaveForm(Form form, FormViewModel model, Dictionary<string, object[]> state, ControllerContext context)
{
using (ApplicationContext.ProfilingLogger.DebugDuration<FormsController>(string.Format("Umbraco Forms: Submitting Form '{0}' with id '{1}'", form.Name, form.Id)))
{
var record = new Record();
if (Session["Forms_RecordID"] != null)
record = GetRecord(new Guid(Session["Forms_RecordID"].ToString()), form);
if (model.RecordId != Guid.Empty)
record = GetRecord(model.RecordId, form);
record.Form = form.Id;
record.State = FormState.PartiallySubmitted;
record.UmbracoPageId = CurrentPage.Id;
record.IP = HttpContext.Request.UserHostAddress;
record.CurrentPage = CurrentPage.Version;
if (HttpContext.User != null && HttpContext.User.Identity.IsAuthenticated && Membership.GetUser() != null)
record.MemberKey = Membership.GetUser().ProviderUserKey.ToString();
GetFieldsAndValues(form, state, context, record);
record.RecordData = record.GenerateRecordDataAsJson();
using (var rs = new RecordStorage())
{
if (record.Id <= 0)
rs.InsertRecord(record, form);
else
rs.UpdateRecord(record, form);
}
RecordService.Instance.AddRecordIdToTempData(record, ControllerContext);
if (Session["Forms_RecordID"] == null)
Session["Forms_RecordID"] = record.UniqueId.ToString();
}
}
private static void GetFieldsAndValues(Form form, Dictionary<string, object[]> state, ControllerContext context, Record record)
{
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
{
var recordField = new RecordField(allField);
recordField.Values.AddRange(array);
record.RecordFields.Add(allField.Id, recordField);
}
}
}
private Record GetRecord(Guid recordId, Form form)
{
Record recordByUniqueId;
using (var recordStorage = new RecordStorage())
recordByUniqueId = recordStorage.GetRecordByUniqueId(recordId, form);
return recordByUniqueId;
}
private void ForwardNext(Form form, FormViewModel model, Dictionary<string, object[]> state)
{
var formStep = model;
formStep.FormStep++;
if (IsMemberSignedIn())
{
SaveForm(form, model, model.FormState, ControllerContext);
}
var isLastPage = model.FormStep == form.Pages.Count();
if (isLastPage)
{
model.SubmitHandled = true;
Session["Forms_RecordID"] = null;
}
}
protected void BackwardPrevious(Form form, FormViewModel model, Dictionary<string, object[]> state)
{
var formStep = model;
formStep.FormStep--;
if (model.FormStep < 0)
model.FormStep = 0;
}
private void StoreFormModel(FormViewModel model)
{
TempData["umbracoformsform"] = model;
}
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;
}
public static event EventHandler<FormEventArgs> FormPrePopulate;
public static event EventHandler<FormValidationEventArgs> FormValidate;
}
Discovered this post last night as I'm trying to implement a Save & Continue function on Umbraco Forms (Umbraco v8, Forms v8.7.6).
I'm seeing all sorts of errors around RecordStorage and RecordService not being accessible due it's protection level. DistributedCache does not contain a definition for instance so I'm guessing this is something which has changed in Forms? (very new to Umbraco Forms).
I'm trying to implement a button/link to Save & Continue later at any stage of the form and on any device by providing site visitors with a unique link (without providing login details) where they can return to complete their form and then submit.
So the fix I posted was for V7 Umbraco which uses and earlier version on Forms. Umbraco changed quite a lot in V8 as far as my understanding goes. However to backward 'engineer' this for my purposes I had to work with a V8 instance...
BTW hopefully you can take something from it, sorry for the presentation.. Didn't copy + paste very well.
Code from V8 project that may help you;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Scoping;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Attributes;
using Umbraco.Forms.Core.Data.Storage;
using Umbraco.Forms.Core.Enums;
using Umbraco.Forms.Core.Extensions;
using Umbraco.Forms.Core.Models;
using Umbraco.Forms.Core.Persistence.Dtos;
using Umbraco.Forms.Core.Providers;
using Umbraco.Forms.Core.Services;
using Umbraco.Forms.Mvc;
using Umbraco.Forms.Mvc.BusinessLogic;
using Umbraco.Forms.Mvc.Models;
using Umbraco.Forms.Web.Models;
using Umbraco.Web;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
namespace UmbracoV8Concept01.App_Code.Controllers
{
public class FormsController : SurfaceController
{
private readonly IFormStorage _formStorage;
private readonly IRecordStorage _recordStorage;
private readonly IRecordService _recordService;
private readonly IFacadeConfiguration _configuration;
private readonly FieldCollection _fieldCollection;
private readonly IFieldTypeStorage _fieldTypeStorage;
private readonly IFieldPreValueSourceService _fieldPreValueSourceService;
private readonly IFieldPreValueSourceTypeService _fieldPreValueSourceTypeService;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IPageService _pageService;
private readonly IScopeProvider _scopeProvider;
private const string FormsFormKey = "umbracoformsform";
public FormsController(
IFormStorage formStorage,
IRecordStorage recordStorage,
IRecordService recordService,
IFacadeConfiguration configuration,
FieldCollection fieldCollection,
IFieldTypeStorage fieldTypeStorage,
IFieldPreValueSourceService fieldPreValueSourceService,
IFieldPreValueSourceTypeService fieldPreValueSourceTypeService,
IUmbracoContextAccessor umbracoContextAccessor,
IPageService pageService,
IScopeProvider scopeProvider)
{
_formStorage = formStorage;
_recordStorage = recordStorage;
_recordService = recordService;
_configuration = configuration;
_fieldCollection = fieldCollection;
_fieldTypeStorage = fieldTypeStorage;
_fieldPreValueSourceService = fieldPreValueSourceService;
_fieldPreValueSourceTypeService = fieldPreValueSourceTypeService;
_umbracoContextAccessor = umbracoContextAccessor;
_pageService = pageService;
_scopeProvider = scopeProvider;
}
private static string Base64Decode(string base64EncodedData)
{
byte[] numArray = Convert.FromBase64String(base64EncodedData);
return Encoding.UTF8.GetString(numArray);
}
private static string Base64Encode(string plainText)=>
Convert.ToBase64String(Encoding.UTF8.GetBytes(plainText));
private void ClearFormModel()
{
TempData.Remove("umbracoformsform");
}
private void ClearFormState(FormViewModel model)
{
model.RecordState = string.Empty;
}
private Dictionary<string, object[]> CreateStateFromRecord(Form form, Record record)
{
Dictionary<string, object[]> strs = new Dictionary<string, object[]>();
foreach (KeyValuePair<Guid, RecordField> recordField in record.RecordFields)
{
Field field = form.AllFields.FirstOrDefault<Field>((Field x) => x.Id == recordField.Value.FieldId);
if (field != null)
{
FieldType fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(field);
Guid id = field.Id;
strs.Add(id.ToString(), fieldTypeByField.ConvertFromRecord(field, recordField.Value.Values).ToArray<object>());
}
}
return strs;
}
private Dictionary<string, object[]> ExtractAllPagesState(FormViewModel model, ControllerContext context, Form form)
{
Dictionary<string, object[]> strs;
Guid id;
object[] objArray;
Dictionary<string, object[]> strs1 = RetrieveFormState(model);
if (strs1 != null)
{
foreach (Field allField in form.AllFields)
{
object[] array = new object[0];
object[] objArray1 = form.AllFields.First((Field f) => f.Id == allField.Id).Values == null ? new object[0] : form.AllFields.First((Field f) => f.Id == allField.Id).Values.ToArray();
if (context.HttpContext.Request.Form.AllKeys.Contains(allField.Id.ToString()))
{
NameValueCollection nameValueCollection = context.HttpContext.Request.Form;
id = allField.Id;
string[] values = nameValueCollection.GetValues(id.ToString());
bool flag = true;
if (values != null)
{
string[] strArrays = values;
int num = 0;
while (num < (int)strArrays.Length)
{
if (string.IsNullOrEmpty(strArrays[num]))
num++;
else
{
flag = false;
break;
}
}
if (flag)
objArray = objArray1;
else
objArray = values;
array = objArray;
}
}
else if (!context.HttpContext.Request.Files.AllKeys.Contains(allField.Id.ToString()))
array = objArray1;
else
{
Field field = form.AllFields.First<Field>((Field f) => f.Id == allField.Id);
FieldType fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(field);
array = fieldTypeByField.ProcessSubmittedValue(field, array, context.HttpContext).ToArray();
}
if (!strs1.ContainsKey(allField.Id.ToString()))
{
id = allField.Id;
strs1.Add(id.ToString(), array);
}
else
{
id = allField.Id;
strs1[id.ToString()] = array;
}
}
strs = strs1;
}
else
strs = null;
return strs;
}
private Dictionary<string, object[]> ExtractCurrentPageState(FormViewModel model, ControllerContext context, Form form)
{
Dictionary<string, object[]> strs = RetrieveFormState(model);
if (strs != null)
{
foreach (FieldsetViewModel fieldset in model.CurrentPage.Fieldsets)
{
foreach (FieldsetContainerViewModel container in fieldset.Containers)
{
foreach (FieldViewModel fieldViewModel in container.Fields)
{
object[] array = new object[0];
if (context.HttpContext.Request.Form.AllKeys.Contains<string>(fieldViewModel.Id))
{
object[] values = context.HttpContext.Request.Form.GetValues(fieldViewModel.Id);
array = values;
}
Field field1 = form.AllFields.First<Field>((Field field) => field.Id.ToString() == fieldViewModel.Id);
FieldType fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(field1);
array = fieldTypeByField.ProcessSubmittedValue(field1, array, context.HttpContext).ToArray<object>();
if (!strs.ContainsKey(fieldViewModel.Id))
strs.Add(fieldViewModel.Id, array);
else
strs[fieldViewModel.Id] = array;
}
}
}
}
return strs;
}
private void ExtractDataFromPages(FormViewModel model, Form form)
{
model.FormState = ExtractPagesState(model, ControllerContext, form);
StoreFormState(model.FormState, model);
ResumeFormState(model, model.FormState, false);
}
private Dictionary<string, object[]> ExtractPagesState(FormViewModel model, ControllerContext context, Form form)
{
Dictionary<string, object[]> strs = RetrieveFormState(model);
if (strs != null)
{
foreach (PageViewModel page in model.Pages)
{
foreach (FieldsetViewModel fieldset in page.Fieldsets)
{
foreach (FieldsetContainerViewModel container in fieldset.Containers)
{
foreach (FieldViewModel fieldViewModel in container.Fields)
{
object[] array = new object[0];
if (context.HttpContext.Request.Form.AllKeys.Contains<string>(fieldViewModel.Id))
{
object[] values = context.HttpContext.Request.Form.GetValues(fieldViewModel.Id);
array = values;
}
Field field1 = form.AllFields.First<Field>((Field field) => field.Id.ToString() == fieldViewModel.Id);
//FieldExtensions.PopulateDefaultValue(field1);
FieldType fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(field1);
array = fieldTypeByField.ProcessSubmittedValue(field1, array, context.HttpContext).ToArray<object>();
if (!strs.ContainsKey(fieldViewModel.Id))
strs.Add(fieldViewModel.Id, array);
else
strs[fieldViewModel.Id] = array;
}
}
}
}
}
return strs;
}
private Form GetForm(Guid formId) =>
_formStorage.GetForm(formId);
private FormViewModel GetFormModel(Guid formId, Guid? recordId, string theme = "")
{
bool flag;
IPublishedContent publishedContent;
if (base.HttpContext.Items["pageElements"] == null)
{
UmbracoContext umbracoContext = _umbracoContextAccessor.UmbracoContext;
if (umbracoContext != null)
{
PublishedRequest publishedRequest = umbracoContext.PublishedRequest;
if (publishedRequest != null)
{
publishedContent = publishedRequest.PublishedContent;
}
else
{
publishedContent = null;
}
}
else
{
publishedContent = null;
}
if (publishedContent != null)
{
HttpContext.Items["pageElements"] = _pageService.GetPageElements();
}
}
if (Session != null)
{
int currentMemberId = -1;
if (Members.IsUmbracoMembershipProviderActive())
{
try
{
currentMemberId = Members.GetCurrentMemberId();
}
catch (Exception exception1)
{
Exception exception = exception1;
Logger.Error(typeof(FormsController), "Can't get the current members Id", new object[] { exception });
}
}
if (currentMemberId <= -1)
{
Session["ContourMemberKey"] = null;
}
else
{
Session["ContourMemberKey"] = currentMemberId;
}
}
FormViewModel formViewModel = RetrieveFormModel();
if ((formViewModel == null ? false : formViewModel.FormId == formId))
{
if (!string.IsNullOrEmpty(theme))
{
formViewModel.Theme = theme;
}
ResumeFormState(formViewModel, formViewModel.FormState, false);
}
else
{
Form form = GetForm(formId);
formViewModel = new FormViewModel();
if (!string.IsNullOrEmpty(theme))
{
formViewModel.Theme = theme;
}
formViewModel.Build(form, _fieldTypeStorage, _fieldPreValueSourceService, _fieldPreValueSourceTypeService);
PrePopulateForm(form, ControllerContext, formViewModel, null);
ResumeFormState(formViewModel, formViewModel.FormState, false);
if (formViewModel.IsFirstPage)
{
ClearFormState(formViewModel);
}
if (!_configuration.AllowEditableFormSubmisisons)
{
ExtractDataFromPages(formViewModel, form);
}
else
{
if (!recordId.HasValue)
{
flag = false;
}
else
{
Guid? nullable = recordId;
Guid empty = Guid.Empty;
if (nullable.HasValue)
{
flag = (nullable.HasValue ? nullable.GetValueOrDefault() != empty : false);
}
else
{
flag = true;
}
}
if (!flag)
{
ExtractDataFromPages(formViewModel, form);
}
else
{
Record record = GetRecord(recordId.Value, form);
if (record != null)
{
PrePopulateForm(form, ControllerContext, formViewModel, record);
formViewModel.RecordId = record.UniqueId;
formViewModel.FormState = CreateStateFromRecord(form, record);
StoreFormState(formViewModel.FormState, formViewModel);
ResumeFormState(formViewModel, formViewModel.FormState, true);
}
}
}
}
List<Guid> guids = new List<Guid>();
if (TempData["UmbracoForms"] != null)
{
guids = (List<Guid>)TempData["UmbracoForms"];
}
if (!guids.Contains(formId))
{
guids.Add(formId);
}
//TempData.set_Item("UmbracoForms", guids);
return formViewModel;
}
private Record GetRecord(Guid recordId, Form form) =>
_recordStorage.GetRecordByUniqueId(recordId, form);
private void GoBackward(FormViewModel model)
{
var formStep = model;
formStep.FormStep -= 1;
if (model.FormStep < 0)
model.FormStep = 0;
}
private void GoForward(Form form, FormViewModel model, Dictionary<string, object[]> state)
{
var formStep = model;
formStep.FormStep += 1;
if (model.FormStep == form.Pages.Count())
SubmitForm(form, model, state, ControllerContext);
}
[HttpPost]
[ValidateFormsAntiForgeryToken]
[ValidateInput(false)]
public ActionResult HandleForm(FormViewModel model)
{
ActionResult umbracoPage;
var form = GetForm(model.FormId);
model.Build(form, _fieldTypeStorage, _fieldPreValueSourceService, _fieldPreValueSourceTypeService);
if (!HoneyPotIsEmpty(model))
model.SubmitHandled = true;
else
{
PrePopulateForm(form, ControllerContext, model, null);
model.FormState = (FormPrePopulate == null ? ExtractCurrentPageState(model, ControllerContext, form) : ExtractAllPagesState(model, ControllerContext, form));
StoreFormState(model.FormState, model);
ResumeFormState(model, model.FormState, false);
if ((!string.IsNullOrEmpty(Request["__prev"]) || !string.IsNullOrEmpty(Request["PreviousClicked"]) ? model.FormStep <= 0 : true))
{
ValidateFormState(model, form, ControllerContext.HttpContext);
if (ModelState.IsValid)
{
GoForward(form, model, model.FormState);
}
}
else
GoBackward(model);
model.IsFirstPage = model.FormStep == 0;
model.IsLastPage = model.FormStep == form.Pages.Count - 1;
}
OnFormHandled(form, model);
StoreFormModel(model);
//return CurrentUmbracoPage();
if ((!model.SubmitHandled ? true : form.GoToPageOnSubmit <= 0))
umbracoPage = CurrentUmbracoPage();
else
{
ClearFormModel();
ClearFormState(model);
umbracoPage = RedirectToUmbracoPage(form.GoToPageOnSubmit);
}
return umbracoPage;
}
private bool HoneyPotIsEmpty(FormViewModel model)
{
var request = Request;
Guid formId = model.FormId;
return string.IsNullOrEmpty(request[formId.ToString().Replace("-", string.Empty)]);
}
protected virtual void OnFormHandled(Form form, FormViewModel model)
{
}
private static void PopulateFieldValues(FormViewModel model, Form form)
{
object[] objArray;
foreach (Field allField in form.AllFields)
{
Dictionary<string, object[]> formState = model.FormState;
Guid id = allField.Id;
formState.TryGetValue(id.ToString(), out objArray);
allField.Values = (objArray != null ? objArray.ToList() : new List<object>());
}
}
private void PrePopulateForm(Form form, ControllerContext context, FormViewModel formViewModel, Record record = null)
{
Guid id;
Dictionary<string, object[]> strs = RetrieveFormState(formViewModel);
object[] value = new object[0];
if (FormPrePopulate != null)
{
foreach (Field allField in form.AllFields)
{
if (!context.HttpContext.Request.Form.AllKeys.Contains(allField.Id.ToString()))
{
KeyValuePair<string, object[]> keyValuePair = strs.FirstOrDefault<KeyValuePair<string, object[]>>((KeyValuePair<string, object[]> v) => v.Key == allField.Id.ToString());
value = keyValuePair.Value;
if (value != null)
{
object[] objArray = value;
for (int i = 0; i < (int)objArray.Length; i++)
{
object obj = objArray[i];
if (allField.Values == null)
allField.Values.Add(new List<object>());
else if (allField.Settings.Keys.Contains("DefaultValue"))
allField.Values.Clear();
if (obj.Equals(string.Empty))
allField.Values.Clear();
else
allField.Values.Add(obj);
}
}
else
continue;
}
else
{
var nameValueCollection = context.HttpContext.Request.Form;
id = allField.Id;
value = nameValueCollection.GetValues(id.ToString());
if (allField.Values == null)
allField.Values.Add(new List<object>());
else if (allField.Settings.Keys.Contains("DefaultValue"))
allField.Values.Clear();
object[] objArray1 = value;
for (int j = 0; j < objArray1.Length; j++)
{
object obj1 = objArray1[j];
if (obj1.Equals(string.Empty))
allField.Values.Clear();
else
allField.Values.Add(obj1);
}
}
if (!strs.ContainsKey(allField.Id.ToString()))
{
id = allField.Id;
strs.Add(id.ToString(), value);
}
else
{
id = allField.Id;
strs[id.ToString()] = value;
}
}
bool? nullable = null;
using (IScope scope = _scopeProvider.CreateScope(IsolationLevel.Unspecified, 0, null, nullable, false, true))
{
scope.Events.Dispatch(FormPrePopulate, this, new FormEventArgs(form), "PrePopulatingForm");
}
if (record != null)
{
foreach (Field field in form.AllFields)
{
object[] array = new object[0];
FieldType fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(field);
array = fieldTypeByField.ConvertToRecord(field, array, ControllerContext.HttpContext).ToArray<object>();
if (record.GetRecordField(field.Id) == null)
{
RecordField recordField = new RecordField(field);
recordField.Values.AddRange(array);
record.RecordFields.Add(field.Id, recordField);
}
}
foreach (KeyValuePair<Guid, RecordField> recordField1 in record.RecordFields)
{
foreach (Field allField1 in form.AllFields)
{
if (allField1.Id == recordField1.Value.FieldId)
{
if (allField1.Values != null)
recordField1.Value.Values.Add(allField1.Values);
}
}
}
}
}
}
//[ChildActionOnly]
//public ActionResult Render(Guid formId, Guid? recordId = null, string view = "", string mode = "full")
//{
// FormViewModel formModel = GetFormModel(formId, recordId, "");
// formModel.RenderMode = mode;
// if (File.Exists(base.get_Server().MapPath(string.Format("{0}/{1}/Form.cshtml", Constants.System.ViewsPath, formModel.FormId))))
// {
// view = Path.Combine(Constants.System.ViewsPath, string.Format("{0}/Form.cshtml", formModel.FormId));
// }
// else if (string.IsNullOrEmpty(view))
// {
// view = Path.Combine(Constants.System.ViewsPath, "Form.cshtml");
// }
// else if ((view.StartsWith("~") ? false : !view.StartsWith("/")))
// {
// view = Path.Combine(Constants.System.ViewsPath, view);
// }
// return PartialView(view, formModel);
//}
[ChildActionOnly]
public ActionResult RenderForm(Guid formId, Guid? recordId = null, string theme = "", bool includeScripts = true)
{
FormViewModel formModel = GetFormModel(formId, recordId, theme);
formModel.RenderScriptFiles = includeScripts;
return PartialView(FormThemeResolver.GetFormRender(formModel), formModel);
}
[ChildActionOnly]
public ActionResult RenderFormScripts(Guid formId, string theme = "")
{
FormViewModel formModel = GetFormModel(formId, null, theme);
return PartialView(FormThemeResolver.GetScriptView(formModel), formModel);
}
private void ResumeFormState(FormViewModel model, Dictionary<string, object[]> state, bool editSubmission = false)
{
if (state != null)
{
foreach (PageViewModel page in model.Pages)
{
foreach (FieldsetViewModel fieldset in page.Fieldsets)
{
foreach (FieldsetContainerViewModel container in fieldset.Containers)
{
foreach (FieldViewModel field in container.Fields)
{
if (editSubmission)
{
if (field.FieldType.Id == Guid.Parse("A72C9DF9-3847-47CF-AFB8-B86773FD12CD"))
{
FieldType item = _fieldCollection[Guid.Parse("DA206CAE-1C52-434E-B21A-4A7C198AF877")];
field.FieldType = item;
field.HideLabel = true;
}
}
if (state.ContainsKey(field.Id))
field.Values = state[field.Id];
}
}
}
}
}
}
private FormViewModel RetrieveFormModel()
{
FormViewModel item;
if (TempData.ContainsKey("umbracoformsform"))
item = TempData["umbracoformsform"] as FormViewModel;
else
item = null;
return item;
}
private Dictionary<string, object[]> RetrieveFormState(FormViewModel model)
{
Dictionary<string, object[]> strs;
if (!string.IsNullOrEmpty(model.RecordState))
{
var str = Base64Decode(model.RecordState);
strs = JsonConvert.DeserializeObject<Dictionary<string, object[]>>(str.DecryptWithMachineKey());
}
else
strs = new Dictionary<string, object[]>();
return strs;
}
private void StoreFormModel(FormViewModel model)
{
TempData["umbracoformsform"] = model;
}
private void StoreFormState(Dictionary<string, object[]> state, FormViewModel model)
{
string str = JsonConvert.SerializeObject(state);
model.RecordState = Base64Encode(str.EncryptWithMachineKey());
}
private void SubmitForm(Form form, FormViewModel model, Dictionary<string, object[]> state, ControllerContext context)
{
Guid id;
bool flag;
string str;
using (DisposableTimer disposableTimer = Current.ProfilingLogger.DebugDuration<FormsController>(string.Format("Umbraco Forms: Submitting Form '{0}' with id '{1}'", form.Name, form.Id)))
{
model.SubmitHandled = true;
Record record = new Record();
if (model.RecordId != Guid.Empty)
{
record = GetRecord(model.RecordId, form);
}
record.Form = form.Id;
record.State = FormState.Submitted;
record.UmbracoPageId = CurrentPage.Id;
record.IP = HttpContext.Request.UserHostAddress;
bool isAuthenticated = false;
string name = null;
var hUser = HttpContext.User;
if ((HttpContext == null || hUser == null ? false : hUser.Identity != null))
{
isAuthenticated = hUser.Identity.IsAuthenticated;
name = hUser.Identity.Name;
}
if ((!isAuthenticated ? false : !string.IsNullOrEmpty(name)))
{
var user = Membership.GetUser(name);
if (user != null)
{
var record1 = record;
object providerUserKey = user.ProviderUserKey;
if (providerUserKey != null)
str = providerUserKey.ToString();
else
str = null;
record1.MemberKey = str;
}
}
foreach (var allField in form.AllFields)
{
object[] item = new object[0];
if (state == null)
flag = false;
else
{
id = allField.Id;
flag = state.ContainsKey(id.ToString());
}
if (flag)
{
id = allField.Id;
item = state[id.ToString()];
}
var fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(allField);
item = fieldTypeByField.ConvertToRecord(allField, item, context.HttpContext).ToArray<object>();
if (record.GetRecordField(allField.Id) == null)
{
var recordField = new RecordField(allField);
recordField.Values.AddRange(item);
record.RecordFields.Add(allField.Id, recordField);
}
else
{
var recordFields = record.GetRecordField(allField.Id).Values;
recordFields.Clear();
recordFields.AddRange(item);
}
}
ClearFormState(model);
_recordService.Submit(record, form);
_recordService.AddRecordIdToTempData(record, ControllerContext);
}
}
private void ValidateFormState(FormViewModel model, Form form, HttpContextBase context)
{
PopulateFieldValues(model, form);
Dictionary<Guid, string> dictionary = form.AllFields.ToDictionary<Field, Guid, string>((Field f) => f.Id, (Field f) => string.Join<object>(", ", f.Values ?? new List<object>()));
foreach (FieldSet fieldSet in form.Pages[model.FormStep].FieldSets)
{
if ((fieldSet.Condition == null ? false : fieldSet.Condition.Enabled))
{
if (!FieldConditionEvaluation.IsVisible(fieldSet.Condition, form, dictionary))
continue;
}
foreach (Field field in fieldSet.Containers.SelectMany((FieldsetContainer c) => c.Fields))
{
var nameValueCollection = context.Request.Form;
var id = field.Id;
string[] values = nameValueCollection.GetValues(id.ToString()) ?? Array.Empty<string>();
var fieldTypeByField = _fieldTypeStorage.GetFieldTypeByField(field);
object obj = fieldTypeByField.ValidateField(form, field, values, HttpContext, _formStorage);
if (obj == null)
obj = new string[0];
foreach (string str in (IEnumerable<string>)obj)
{
string str1 = str;
if (string.IsNullOrWhiteSpace(str))
{
//str1 = StringExtensions.ParsePlaceHolders( form.InvalidErrorMessage ?? string.Empty,field.Caption);
str1 = field.Caption;
}
var modelState = ModelState;
id = field.Id;
modelState.AddModelError(id.ToString(), str1);
}
}
}
FormValidate?.Invoke(this, new FormValidationEventArgs(form));
}
public static event EventHandler<FormEventArgs> FormPrePopulate;
public static event EventHandler<FormValidationEventArgs> FormValidate;
}
You also might have to add a routes file in the App_Code/Routes/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Umbraco.Core.Composing;
using Umbraco.Forms.Core;
using Umbraco.Forms.Web.Controllers;
namespace UmbracoV8Concept01.App_Code
{
public class CustomRoutesComposer : ComponentComposer
public class CustomRoutesComponent : IComponent
{
public void Initialize()
{
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
};
}
public void Terminate()
{
throw new NotImplementedException();
}
}
Wow...Duncan thank you for posting so quickly, I'll give this a go and post back with how I get on, I've popped the controller in and no errors on build so that's a good sign :)
Hey Stuart, I replied to one of your earlier threads asking if you got this working, but it looks like you did... did you have to change anything in the above code at all or is it all working as you expected?
Hey Duncan, I got around to testing this... does this work for you? I was expecting the record to be saved to the database when the user clicks next or previous, but that isn't the case.
I modified the SubmitForm method to accept a FormState param like so:
Then I changed the GoForward method to this, so that it saves the record as PartiallySubmitted when you step forward, but it always saves the record to the database as Accepted - it seems to ignore whatever value I pass in.
Sorry I missed your comment on the other thread, I was trying to implement this but then had to step away to complete other work.
Still very much interested to go back and get the solution working though, I have a very slightly different objective where I want to display a 'Save Progress' button/link to write the partially submitted form to the database rather than the it being written during the Next or Previous clicks.
When clicked, I also aim to then try and generate a link which the visitor can use to revisit their form on any device and pick up from where they left off...but I'm going to play around with what you've done above too and see if I can make progress...thanks for posting.
No problem, I think I got you and Duncan mixed up at one point. Anyway :)
Actually that's the scenario I'm looking at as well... basically I need logged in members to be able to save progress of a form at any moment. The problem with doing it on Next/Previous is that those methods validate the form, and I need to disable validation for the purposes of saving progress.
I was just trying to work with the GoForward method for the time being.
Yea, it doesn't save to the DB for me unless I make the change to the GoForward method as I've mentioned above. And even then the status is wrong (always Accepted).
Is your bullet list there how the vanilla project you linked should work?
I reverted the GoForward method, and tried making those changes to the SubmitForm method, and it didn't save to DB. When I put my changes back into GoForward, it saves a new record to the DB for each step... but all as Approved.
Would you expect it to save a new record to the DB on each step? I guess that makes sense, I just can't get it to not be Approved.
Have to admit, this feels like something that should be supported out of the box.
The records get saved in the UFRecords table as STATE 'PartialySubmitted' The Form entry stays the same (obvs)... UniqueId changes...
But for each 'PartiallySubmittted' entry there is a new record created.
But yes totally agree with you saying it should be supported out the box... I mean I had to look at more recent editions to then go backwards and FIX the functionality...
I can only assume this is environmental then... do you have the umbraco.sdf for your test website? It wasn't in the repo so I had to create a new instance, I'm wondering if something has gone wrong there.
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.
Hi Satpal Gahir
I know this is a long shot but do you still have the code you achieved this feature in, I would like to see some of it if possible.
I have version Umbraco Forms: 8.6.0
I managed to get this working on Umbraco 7, Forms Version 7.1.3.
I like many others had issues with the code above. What I did was look at Umbraco 8 and it's version of Forms, this worked. Using Telerick JustDecompile I implemented the newer version of the code to my older version of Umbraco.
Hope this helps?
Cheers
Hi Duncan,
Discovered this post last night as I'm trying to implement a Save & Continue function on Umbraco Forms (Umbraco v8, Forms v8.7.6).
I'm seeing all sorts of errors around RecordStorage and RecordService not being accessible due it's protection level. DistributedCache does not contain a definition for instance so I'm guessing this is something which has changed in Forms? (very new to Umbraco Forms).
I'm trying to implement a button/link to Save & Continue later at any stage of the form and on any device by providing site visitors with a unique link (without providing login details) where they can return to complete their form and then submit.
Any help/advice would be greatly appreciated.
Cheers, Stuart
Hi Stuart,
So the fix I posted was for V7 Umbraco which uses and earlier version on Forms. Umbraco changed quite a lot in V8 as far as my understanding goes. However to backward 'engineer' this for my purposes I had to work with a V8 instance...
BTW hopefully you can take something from it, sorry for the presentation.. Didn't copy + paste very well.
Code from V8 project that may help you;
using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Data; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Principal; using System.Text; using System.Threading; using System.Web; using System.Web.Mvc; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; using Umbraco.Forms.Core; using Umbraco.Forms.Core.Attributes; using Umbraco.Forms.Core.Data.Storage; using Umbraco.Forms.Core.Enums; using Umbraco.Forms.Core.Extensions; using Umbraco.Forms.Core.Models; using Umbraco.Forms.Core.Persistence.Dtos; using Umbraco.Forms.Core.Providers; using Umbraco.Forms.Core.Services; using Umbraco.Forms.Mvc; using Umbraco.Forms.Mvc.BusinessLogic; using Umbraco.Forms.Mvc.Models; using Umbraco.Forms.Web.Models; using Umbraco.Web; using Umbraco.Web.Mvc; using Umbraco.Web.Routing; using Umbraco.Web.Security;
namespace UmbracoV8Concept01.App_Code.Controllers { public class FormsController : SurfaceController { private readonly IFormStorage _formStorage;
}
You also might have to add a routes file in the App_Code/Routes/
using System.Collections.Generic; using System.Linq; using System.Web; using Umbraco.Core.Composing; using Umbraco.Forms.Core; using Umbraco.Forms.Web.Controllers;
namespace UmbracoV8Concept01.App_Code { public class CustomRoutesComposer : ComponentComposer
}
Wow...Duncan thank you for posting so quickly, I'll give this a go and post back with how I get on, I've popped the controller in and no errors on build so that's a good sign :)
Thanks, Stuart
Hey Stuart, I replied to one of your earlier threads asking if you got this working, but it looks like you did... did you have to change anything in the above code at all or is it all working as you expected?
Hi Greg,
If you need a vanilla project for this, I have uploaded my implemented version to GitHub. It is version 8.
https://github.com/duncan2dg/Umbraco8MultiPageFormSave
Aw amazing! Saved me some time :D
Hey Duncan, I got around to testing this... does this work for you? I was expecting the record to be saved to the database when the user clicks next or previous, but that isn't the case.
I modified the SubmitForm method to accept a FormState param like so:
private void SubmitForm(Form form, FormViewModel model, Dictionary<string, object[]> state, ControllerContext context, FormState formState)
and changed this line:
record.State = FormState.Submitted;
to
record.State = formState;
Then I changed the GoForward method to this, so that it saves the record as PartiallySubmitted when you step forward, but it always saves the record to the database as Accepted - it seems to ignore whatever value I pass in.
Any ideas?
Hi Greg,
Sorry I missed your comment on the other thread, I was trying to implement this but then had to step away to complete other work.
Still very much interested to go back and get the solution working though, I have a very slightly different objective where I want to display a 'Save Progress' button/link to write the partially submitted form to the database rather than the it being written during the Next or Previous clicks.
When clicked, I also aim to then try and generate a link which the visitor can use to revisit their form on any device and pick up from where they left off...but I'm going to play around with what you've done above too and see if I can make progress...thanks for posting.
Stuart
No problem, I think I got you and Duncan mixed up at one point. Anyway :)
Actually that's the scenario I'm looking at as well... basically I need logged in members to be able to save progress of a form at any moment. The problem with doing it on Next/Previous is that those methods validate the form, and I need to disable validation for the purposes of saving progress.
I was just trying to work with the GoForward method for the time being.
I will come back to this over the weekend.
Hi Greg,
Sorry for the late reply..
As far as I am aware... Just looking through it again to refresh my memory;
Signed in member uses form
Selects Next
Saves instance to DB
Selects Next
Repeats/updates instance to DB
Selects Submit
Saves instance to DB marked as Submitted
A GUID is created with can be used to update the form at a later date as a signed in member.
Are you still fighting with this?
Yea, it doesn't save to the DB for me unless I make the change to the GoForward method as I've mentioned above. And even then the status is wrong (always Accepted).
Is your bullet list there how the vanilla project you linked should work?
can you try and remove:
model.SubmitHandled = true;
and
ClearFormState(model);
from SubmitForm()
And see what happens?
Sorry in advance, I have to admit is has been a while since I looked at this..
Is your bullet list there how the vanilla project you linked should work?
And yes, this is a simplified workflow of the process.
Don't be sorry, I appreciate the help.
I reverted the GoForward method, and tried making those changes to the SubmitForm method, and it didn't save to DB. When I put my changes back into GoForward, it saves a new record to the DB for each step... but all as Approved.
Would you expect it to save a new record to the DB on each step? I guess that makes sense, I just can't get it to not be Approved.
Have to admit, this feels like something that should be supported out of the box.
The records get saved in the UFRecords table as STATE 'PartialySubmitted' The Form entry stays the same (obvs)... UniqueId changes...
But for each 'PartiallySubmittted' entry there is a new record created.
But yes totally agree with you saying it should be supported out the box... I mean I had to look at more recent editions to then go backwards and FIX the functionality...
I can only assume this is environmental then... do you have the umbraco.sdf for your test website? It wasn't in the repo so I had to create a new instance, I'm wondering if something has gone wrong there.
Hi Greg,
Wondering if you managed to figure this out and how to have the record saved as "PartiallySubmitted" instead of "Approved"
is working on a reply...