Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Moshe Recanati 8 posts 88 karma points
    Aug 21, 2017 @ 05:28
    Moshe Recanati
    0

    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

  • Jamie Brunton 3 posts 73 karma points
    Aug 08, 2018 @ 15:12
    Jamie Brunton
    0

    Did you find a solution to this?

  • J 351 posts 606 karma points
    Jun 04, 2019 @ 10:39
    J
    0

    Would be great to know if anyone found a solution to this?

  • Tom van Enckevort 100 posts 387 karma points
    Jun 06, 2019 @ 07:40
    Tom van Enckevort
    2

    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.

  • Damiaan 438 posts 1290 karma points MVP 3x c-trib
    Jun 06, 2019 @ 07:46
  • J 351 posts 606 karma points
    Jun 06, 2019 @ 12:29
    J
    0

    Thanks Tom, Im using Umbraco 7.5 with Forms 4.4.7.

    I dont seem to have the event FormPrePopulate

    Tried using

    Umbraco.Forms.Web.Controllers.UmbracoFormsController.FormPrePopulate
    

    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

  • Tom van Enckevort 100 posts 387 karma points
    Jun 06, 2019 @ 13:01
    Tom van Enckevort
    1

    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.

  • J 351 posts 606 karma points
    Jun 06, 2019 @ 15:52
    J
    0

    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.

    Any other thoughts?

  • Tom van Enckevort 100 posts 387 karma points
    Jun 06, 2019 @ 16:14
    Tom van Enckevort
    3

    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:

    @using (Html.BeginUmbracoForm<MyProject.Controllers.Surface.FormsController>("HandleFormSubmission"))
    

    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).

  • J 351 posts 606 karma points
    Jun 07, 2019 @ 09:59
    J
    0

    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.

  • Jamie Townsend 26 posts 174 karma points c-trib
    Aug 09, 2019 @ 12:18
    Jamie Townsend
    1

    @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.

    SaveForm(form, model, model.FormState, ControllerContext, out var recordId);
    

    .

    private void SaveForm(Form form, FormViewModel model, Dictionary<string, object[]> state, ControllerContext context, out string recordId)
        {           
            // 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);
                recordId = record.UniqueId.ToString();
            }
        }
    

    Thanks again

  • Ash 18 posts 88 karma points
    Sep 19, 2019 @ 16:21
    Ash
    0

    That looks really helpful. What version of Umbraco Forms did you try this on please?

    thanks

  • Tom van Enckevort 100 posts 387 karma points
    Sep 19, 2019 @ 16:33
    Tom van Enckevort
    0

    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

  • Ash 18 posts 88 karma points
    Sep 20, 2019 @ 08:07
    Ash
    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.

  • Ash 18 posts 88 karma points
    Sep 20, 2019 @ 13:31
    Ash
    0

    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());

  • Tom van Enckevort 100 posts 387 karma points
    Sep 20, 2019 @ 13:53
    Tom van Enckevort
    1

    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.

  • Ash 18 posts 88 karma points
    Sep 25, 2019 @ 15:55
    Ash
    0

    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

  • Tony 104 posts 162 karma points
    Oct 23, 2019 @ 11:05
    Tony
    0

    I'm working this into my current project, would you be able to post up the final code? thank you

  • Tony 104 posts 162 karma points
    Oct 23, 2019 @ 13:26
    Tony
    0

    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

  • Ash 18 posts 88 karma points
    Oct 23, 2019 @ 13:44
    Ash
    0

    Hi Tony,

    Post your controller code here mate. Let's see what's going on.

  • Tony 104 posts 162 karma points
    Oct 23, 2019 @ 13:47
    Tony
    0

    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.

  • Ash 18 posts 88 karma points
    Oct 23, 2019 @ 13:59
    Ash
    0

    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.

    Hope that helps

  • Tony 104 posts 162 karma points
    Oct 23, 2019 @ 16:01
    Tony
    0

    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

    if (Request["next"] != null)
    {
        ForwardNext(form, model, model.FormState);
    }
    else
    {
        SaveForm(form, model, model.FormState, ControllerContext);
    }
    
  • Ash 18 posts 88 karma points
    Oct 23, 2019 @ 16:21
    Ash
    0

    Call from HandleFormSubmission method :

                    if (ModelState.IsValid)
                    {
                        ForwardNext(form, model, model.FormState);
                    }
    
  • Tony 104 posts 162 karma points
    Dec 04, 2019 @ 11:35
    Tony
    0

    Sorry ash, Ive come back to this after stepping away for a bit. Whereabouts in the handleformsubmit does this go?

    if (ModelState.IsValid)
    {
         ForwardNext(form, model, model.FormState);
    }
    

    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?

  • Tony 104 posts 162 karma points
    Oct 23, 2019 @ 16:38
    Tony
    0

    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

  • Ash 18 posts 88 karma points
    Oct 23, 2019 @ 17:00
    Ash
    0

    If u open chrome dev tools and look at the request and see if this 'next' is present in the request.

  • Tony 104 posts 162 karma points
    Oct 23, 2019 @ 21:11
    Tony
    0

    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.

  • Ash 18 posts 88 karma points
    Oct 24, 2019 @ 09:58
    Ash
    0

    Have you found solution to this yet on 7.2?

    If not ,

    Downgrade to 7.1.x you should be able use "next"

  • Tony 104 posts 162 karma points
    Oct 24, 2019 @ 10:39
    Tony
    0

    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.!

    enter image description here

  • Ash 18 posts 88 karma points
    Oct 24, 2019 @ 17:03
    Ash
    0

    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.

  • Ash 18 posts 88 karma points
    Oct 24, 2019 @ 17:07
    Ash
    0

    however if u install 7.1.3 version of forms u should get Request["next"]

  • Satpal Gahir 18 posts 88 karma points
    Nov 26, 2019 @ 17:55
    Satpal Gahir
    0

    @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.

  • Ash 18 posts 88 karma points
    Nov 26, 2019 @ 18:09
    Ash
    0

    Check my posts above with code and implement forward next methods

  • Tony 104 posts 162 karma points
    Dec 04, 2019 @ 12:16
    Tony
    1

    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.!

  • Satpal Gahir 18 posts 88 karma points
    Dec 04, 2019 @ 13:07
    Satpal Gahir
    0

    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.

  • 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.

Please Sign in or register to post replies