Pages

Tuesday, November 16, 2021

Optimizely Form Container Block with Razor View

With CMS 11, Optimizely Forms display using a traditional Webform User Control (ASCX) out of the box. With all the work I have been doing with forms lately I wanted to change this behaviour to use a Razor View instead. Now that I have it working, I wanted to share it. 

Take note: as mentioned this only applies to CMS 11. With the release of CMS 12, Optimizely has rewritten Forms views to now utilize Razor, and all this work is not necessary.

First, grab the code for the Razor view

I rewrote the existing FormContainerBlock.ascx file as a Razor View. During the process I modified a bit of the code to clean up some areas. Most of the code is the same, just converted to Razor syntax, and I kept comments as well. Overall, it functions the same as the out-of-box Webforms version. Grab the code below, or copy/download it from my Gist, and save it as a .CSHTML file in a folder of your choosing. I like to keep my Views organized immediately below the Views folder, so mine lives as /Views/Forms/FormContainerBlock.cshtml.

[pre class="brush:razor;class-name:collapse-box;"]
@using System.Web.Mvc
@using EPiServer.Editor
@using EPiServer.Web.Mvc.Html
@using EPiServer.Shell.Web.Mvc.Html
@using EPiServer.Forms
@using EPiServer.Forms.Core
@using EPiServer.Forms.Helpers.Internal
@using EPiServer.Forms.EditView.Internal
@using EPiServer.Forms.Implementation.Elements
@using EPiServer.Forms.Implementation.Elements.BaseClasses
@using BaseEpiserverSite.Models.Forms

@model StandardFormContainerBlock

@{
    var _formConfig = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<EPiServer.Forms.Configuration.IEPiServerFormsImplementationConfig>();
    var validationCssClass = ViewBag.ValidationFail ?? false ? "ValidationFail" : "ValidationSuccess";
    var _formStartTag = ViewBag.RenderingFormUsingDivElement ? $"<div data-f-metadata=\"{ Model.MetadataAttribute} \" class=\"EPiServerForms {validationCssClass}\" data-f-type=\"form\" id=\"{Model.Form.FormGuid}\">"
        : $"<form method=\"post\" novalidate=\"novalidate\" data-f-metadata=\"{Model.MetadataAttribute}\" enctype=\"multipart / form - data\" class=\"EPiServerForms {validationCssClass}\" data-f-type=\"form\" id=\"{Model.Form.FormGuid}\">";
    var _formEndTag = ViewBag.RenderingFormUsingDivElement ? "</div>" : "</form>";
}

@if (PageEditing.PageIsInEditMode)
{
    <link rel="stylesheet" type="text/css" data-f-resource="EPiServerForms.css" href="@ModuleHelper.ToClientResource(typeof(FormsModule), "ClientResources/ViewMode/EPiServerForms.css")" />
    if (Model.Form != null)
    {
        <div class="EPiServerForms">
            <h2 class="Form__Title">@Html.PropertyFor(m => m.Title)</h2>
            <h4 class="Form__Description">@Html.PropertyFor(m => m.Description)</h4>

            @Html.PropertyFor(m => m.ElementsArea)
        </div>
    }
    else
    {
        @* In case FormContainerBlock is used as a property, we cannot build Form model so we show a warning message to notify user *@
        <div class="EPiServerForms">
            <span class="Form__Warning">@Html.Translate("/episerver/forms/editview/cannotbuildformmodel")</span>
        </div>
    }
}
else if (Model.Form != null)
{
    @* Using form tag (instead of div) for the sake of html elements' built-in features e.g. reset, file upload
           Using enctype="multipart/form-data" for post data and uploading files
           Start tag logic sits at top of file

        Form will post to its own page Controller *@
    @Html.Raw(_formStartTag)

    //Meta data, authoring data of this form is transfer to clientside here. We need to take form with language coresponse with current page's language
    <script type="text/javascript" src="@($"{_formConfig.CoreController}/GetFormInitScript?formGuid={Model.Form.FormGuid}&formLanguage={FormsExtensions.GetCurrentFormLanguage(Model)}")"></script>

    //Meta data, send along as a SYSTEM information about this form, so this can work without JS
    <input type="hidden" class="Form__Element Form__SystemElement FormHidden FormHideInSummarized" name="__FormGuid" value="@Model.Form.FormGuid" data-f-type="hidden" />
    <input type="hidden" class="Form__Element Form__SystemElement FormHidden FormHideInSummarized" name="__FormHostedPage" value="@FormsExtensions.GetCurrentPageLink().ToString()" data-f-type="hidden" />
    <input type="hidden" class="Form__Element Form__SystemElement FormHidden FormHideInSummarized" name="__FormLanguage" value="@FormsExtensions.GetCurrentFormLanguage(Model)" data-f-type="hidden" />
    <input type="hidden" class="Form__Element Form__SystemElement FormHidden FormHideInSummarized" name="__FormCurrentStepIndex" value="@(ViewBag.CurrentStepIndex ?? " ")>" data-f-type="hidden" />
    <input type="hidden" class="Form__Element Form__SystemElement FormHidden FormHideInSummarized" name="__FormSubmissionId" value="@ViewBag.FormSubmissionId" data-f-type="hidden" />
    @Html.GenerateAntiForgeryToken(Model)

    if (!string.IsNullOrWhiteSpace(Model.Title))
    {
        <h2 class="Form__Title">@Model.Title</h2>
    }
    if (!string.IsNullOrWhiteSpace(Model.Description))
    {
        <aside class="Form__Description">@Model.Description</aside>
    }

    var statusDisplay = "hide";
    var message = ViewBag.Message;

    if (ViewBag.FormFinalized || ViewBag.IsProgressiveSubmit)
    {
        statusDisplay = "Form__Success__Message";
    }
    else if (!ViewBag.Submittable && !string.IsNullOrWhiteSpace(message))
    {
        statusDisplay = "Form__Warning__Message";
    }

    if (ViewBag.IsReadOnlyMode)
    {
        <div class="Form__Status">
            <span class="Form__Readonly__Message">
                @Html.Translate("/episerver/forms/viewmode/readonlymode")
            </span>
        </div>
    }

    @* area for showing Form's status or validation *@
    <div class="Form__Status">
        <div class="Form__Status__Message @statusDisplay" data-f-form-statusmessage>
            @Html.Raw(message)
        </div>
    </div>

    <div data-f-mainbody class="Form__MainBody">
        @{
            var currentStepIndex = ViewBag.CurrentStepIndex == null ? -1 : (int)ViewBag.CurrentStepIndex;
            string stepDisplaying;
            foreach (var step in Model.Form.Steps.Select((m, i) => new { Index = i, Item = m }))
            {
                stepDisplaying = (currentStepIndex == @step.Index && !ViewBag.FormFinalized && (bool)ViewBag.IsStepValidToDisplay) ? "" : "hide";
                <section id="@step.Item.ElementName" data-f-type="step" data-f-element-name="@step.Item.ElementName" class="Form__Element FormStep Form__Element--NonData @stepDisplaying" data-f-stepindex="@step.Index" data-f-element-nondata>
                    @{
                        var stepBlock = (step.Item.SourceContent as ElementBlockBase);
                        if (stepBlock != null)
                        {
                            Html.RenderContentData(step.Item.SourceContent, false);
                        }


                        <!-- Each FormStep groups the elements below it til the next FormStep -->
                        Html.RenderElementsInStep(step.Index, step.Item.Elements);
                    }
                </section>
            }
        }

        @{
            // show Next/Previous buttons when having Steps > 1 and navigationBar when currentStepIndex is valid
            var currentDisplayStepCount = Model.Form.Steps.Count();
            if (currentDisplayStepCount > 1 && currentStepIndex > -1 && currentStepIndex < currentDisplayStepCount && !ViewBag.FormFinalized)
            {
                string prevButtonDisableState = (currentStepIndex == 0) || !ViewBag.Submittable ? "disabled" : "";
                string nextButtonDisableState = (currentStepIndex == currentDisplayStepCount - 1) || !ViewBag.Submittable ? "disabled" : "";


                if (Model.ShowNavigationBar)
                {
                    <nav role="navigation" class="Form__NavigationBar" data-f-type="navigationbar" data-f-element-nondata>
                        <button type="submit" name="submit" value="@SubmitButtonType.PreviousStep.ToString()" class="Form__NavigationBar__Action FormExcludeDataRebind btnPrev"
                                @prevButtonDisableState data-f-navigation-previous>
                            @Html.Translate("/episerver/forms/viewmode/stepnavigation/previous")
                        </button>

                        @{
                            // calculate the progress style on-server-side
                            var currentDisplayStepIndex = currentStepIndex + 1;
                            var progressWidth = (100 * currentDisplayStepIndex / currentDisplayStepCount) + "%";
                        }

                        <div class="Form__NavigationBar__ProgressBar">
                            <div class="Form__NavigationBar__ProgressBar--Progress" style="width: @progressWidth" data-f-navigation-progress></div>
                            <div class="Form__NavigationBar__ProgressBar--Text">
                                <span class="Form__NavigationBar__ProgressBar__ProgressLabel">@Html.Translate("/episerver/forms/viewmode/stepnavigation/page")</span>
                                <span class="Form__NavigationBar__ProgressBar__CurrentStep" data-f-navigation-currentstep>@currentDisplayStepIndex</span>/
                                <span class="Form__NavigationBar__ProgressBar__StepsCount" data-f-navigation-stepcount>@currentDisplayStepCount</span>
                            </div>
                        </div>
                        <button type="submit" name="submit" value="@SubmitButtonType.NextStep.ToString()" class="Form__NavigationBar__Action FormExcludeDataRebind btnNext"
                                @nextButtonDisableState data-f-navigation-next>
                            @Html.Translate("/episerver/forms/viewmode/stepnavigation/next")
                        </button>
                    </nav>
                }
            }
        }
    </div>

    @Html.Raw(_formEndTag)
}
[/pre]

Follow my instructions for creating a Custom Form Control Block

My other article, Custom Optimizely Form Containers - A Slightly Different Approach, walks through the approach needed for this. When creating the controller, instead of changing the ViewName to the ASCX file, point it to the Razor file you just created.

[pre class="brush:csharp;"]
result.ViewName = "/Views/Forms/FormContainerBlock.cshtml";
[/pre]

Finally, make sure your Models match

If you follow my other article explicitly you will have a StandardFormContainerBlock model. This will match the code for the form Razor view already. If you change your model type, make sure you update it in the TemplateDescriptor for the controller, and the Razor view.

You should now be able to create a new form using your new custom Form Container and have it process through the new controller and out with the Razor view. 



Because the class names and overall structure is the same as the existing ASCX file, my script for a Validation Summary still works on this.

2 comments:

  1. Be aware your statement "Optimizely Forms display using a traditional Webform User Control (ASCX) out of the box" only applies for pre CMS 12. In CMS 12 (the .NET 5 version) the whole forms packages has moved to razor files and away from user controls as is the requirement for .NET 5. So you might want to preface the article with this info

    ReplyDelete
    Replies
    1. That's a good point. I will have to add an update to this post to indicate that. Most of my posts have been directed to Pre-CMS12 topics or techniques, and I haven't thought about the need to indicate that in the posts yet. But this is a good point to bring up.
      Thanks for the feedback.

      Delete

Share your thoughts or ask questions...