Sitecore:从 Web 表单转换为 MVC 的动态表单

Sitecore: Dynamic Forms converting from Web Forms to MVC

我正在从 sitecore 7.2 升级到 sitecore 8.1。我有 5 年的 Web 窗体经验,但接触 MVC 的时间只有几个月。除了升级到 8.1 之外,我的老板还想从 Web 窗体更改为 MVC。我的公司在 Sitecore 中有一些复杂的表单逻辑,其中有 Asp.Net Web 表单,其中每个表单字段都是它自己的子布局(用户控件)。这允许内容编辑器根据业务给出的要求包括、不包括和重新排序表单字段。这是我在 Web 表单中完成此操作的方法(顺便说一句,我们也使用 GlassMapper): 表单标记代码片段:

<div id="formInputSection" runat="server">
    <div id="fields">
        <p class="required italic <%=reqFieldTextColor %>">
            <sc:Text ID="formReqFields" Field="Required Fields Text" runat="server" ClientIDMode="Static" DataSource="<%#lpOptions.Paths.FullPath %>" />
        </p>

        <asp:UpdatePanel ID="formUpdatePanel" runat="server">
            <ContentTemplate>
                <asp:Panel ID="formPanel" runat="server">
                    <asp:ValidationSummary ID="valSumFormSubmit" runat="server" DisplayMode="BulletList" ValidationGroup="formSubmit" CssClass="errorMsg" />
                    <div>
                        <sc:Placeholder ID="FormFieldsSect" Key="v2_ph_form_fields_col1" runat="server" />
                    </div>
                </asp:Panel>
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>        

    <div id="form_action_submit" runat="server" class="form-action submit">
        <asp:LinkButton ID="btnSubmitForm1" CssClass="form-input submit white" OnClientClick="ValidateAndDisableButton();" OnClick="submit_Click" UseSubmitBehavior="false" runat="server" ClientIDMode="Static" Text="<%#Model.Form_Submit_Text %>" ValidationGroup="formSubmit" />
        <sc:Link ID="pgEditorFormSubmit1" Field="Editor Confirmation Link" CssClass="form-input submit white" runat="server" DataSource="<%#lpOptions.Paths.FullPath %>" Visible="false">
            <sc:Text Field="Form Submit Text" ID="pgEditorSubmitText1" runat="server" ClientIDMode="Static" DataSource="<%#lpOptions.Paths.FullPath %>" Visible="false" />
        </sc:Link>
    </div>

Above, is the important part of the FormSublayout, that includes the an UpdatePanel that contains the placeholder for the individual form field sublayouts. As you can see I also have dynamic validation based on what fields you add into the "v2_ph_form_fields_col1" placeholder.

接下来是基本字段子布局之一的标记。我将使用 FirstName...

    <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="FirstNameField.ascx.cs" Inherits="MyNamespace.FirstName" %>
    <%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>
    <div class="clearfix form-input field text <%=LabelStyle %>">
        <asp:Label ID="Label1" AssociatedControlID="txtFirstName" Text="<%#Editable(x => x.First_Name) %>" runat="server" />
        <asp:TextBox ID="txtFirstName" runat="server" ClientIDMode="Static" type="text"></asp:TextBox>
        <asp:RequiredFieldValidator ID="validFirstName" runat="server" ControlToValidate="txtFirstName" ErrorMessage="<%#Model.First_Name_Required %>"
            Enabled="true" ValidationGroup="formSubmit" Display="None"></asp:RequiredFieldValidator>    
    </div>
    <br />

This field sublayout like all of our fields is stand alone. It can be added to any of our forms and function without error. The Code Behind writes its input to session and on submit we take all values in session and map it to our contact object which would be our Model in MVC...

public partial class FirstName : InheritsFromGlassUserControl<FormFields>
{
    protected override void Page_Load(object sender, EventArgs e)
    {
        if (this.Visible == true)
        {                
            SitecoreContext context = new SitecoreContext();
            Model = context.GetItem<FormFields>(Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource).ID.Guid);
//ls is an object that we grab from session to update and put back into session
//to handle cross user control communication and such
            ls = GetSession();    
            LabelStyle = ls.MergeLabels ? "merge-label" : string.Empty;
            if (!string.IsNullOrWhiteSpace(txtFirstName.Text))
            {
                ls.CurrentLead.FirstName = txtFirstName.Text;
            }
            else
            {
                if (!IsPostBack)
                {
                    if (!string.IsNullOrWhiteSpace(ls.CurrentLead.FirstName) && !ls.IsReferralForm)
                        txtFirstName.Text = ls.CurrentLead.FirstName;
                }
            }       
//Put updated values in "ls" back into session for the next field to update, with its input        
            SessionDetails = ls;
            SetRenderingParameters();
        }
        this.DataBind();
    }
}

The question I have is what would be the best way to implement such a solution, in MVC? I don't have much experience in MVC and rather than just code up a sloppy solution I would like to know if someone had a more best practices example of how they would(have) implement(ed) stand alone form fields given the BeginForm() in MVC posts to a model, yet the glassmapper "model" is responsible for the field label and field validation error message content that is populated from sitecore. I haven't been able to get both models coexisting on the same cshtml file. I know this is complicated but the concept of plug and play form fields is a very valuable solution for businesses. And I figured out one that works for me in Web Forms; just having trouble wrapping my head around doing it in MVC.

好吧,上周我找到了这个问题的答案,在我有时间彻底解释之前我不想post它。所以就像我在问题中所说的那样,我们的表单只是 sitecore 占位符,我们在所有表单中重复使用相同的表单字段用户控件(子布局)。所以在 MVC 中我想要类似的东西。所以你在这个表单示例中看到,我们只有一个占位符,就像 WebForms 版本一样。 *注意稍后对 baseformhelper.js 的引用。

@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Contact_Form>
@{

}
@using (Html.BeginRouteForm(Sitecore.Mvc.Configuration.MvcSettings.SitecoreRouteName,
    FormMethod.Post))
{
    @Html.Sitecore().FormHandler("ContactForm", "SubmitContact")
    <div class="bottom-form-wrapper green-bg">
        <p class="form-title white">@Editable(x => x.Form_Headline_Text)</p>
        <p class="required white italic">@Editable(x => x.Required_Fields_Text)</p>
        <div id="divForm" class="twocol-bottom-form">
            @Html.Sitecore().Placeholder("v2_ph_formfields_col1")
        </div>
    <div class="clearfix"></div>
        <div id="form_action_submit" class="form-action submit">
            <input type="submit" id="lnkSubmitForm1" class="form-input submit red-bg white" value="@Editable(x => x.Submit_Button_Text)" />
        </div>
    </div><!--end form-wrapper-->    
<script type="text/javascript" src="~/scripts/Base-LP/BaseFormHelper.js" ></script >
}
...

Before getting to the first name example I need to explain that my model is inheriting from the partial class that Glass Mapper created from the item template responsible for holding the Label Text and Validation Messages for my form fields. So instead of each user control saving it's value to session, each partial view is updating the Model State with it's own value.

//Contact model that inherits from glass mapper class
public class Contact : FormFields
{
    #region Properties

    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }
...

Because of my model's inheritance, I'm able to access the properties of the glass class I need to populate the form field's label and validation message from sitecore while still posting to the proper contact object when the submit button of my form is hit.

@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@model Models.Contact
@{
//First_Name property comes from Contact class
//FirstName property comes form FormFields Glass Mapper Class
}

<div id="form_input_firstname" class="clearfix form-input text">
    @Html.LabelFor(x => x.FirstName, Model.First_Name)
    @Html.TextBoxFor(x => x.FirstName, new { id = "txtFirstName" })
    @Html.ValidationMessageFor(x => x.FirstName, null, new { id = "validFirstName" })
</div>
<script type="text/javascript" async>
//FirstName Validation Message
function FirstNameValidationMsg() {
    var myMsgNode = document.getElementById('validFirstName');
    //Find span that contains validation message
    if (myMsgNode.childElementCount > 0)
        myMsgNode.children[0].innerHTML = '@Model.First_Name_Required';//Overwrite validation with dynamic message from sitecore
}
</script>

Now, in Web Forms I used "asp:RequiredFieldValdidatior", and in MVC I'm using a combination of jquery.validate with FoolProof Validation for MVC. With the first name field I don't need FoolProof validation but I did add a line to the onError function in jquery.validate.unobtrusive.js file so that sitecore has the final say on what the validation message will be.

function onError(error, inputElement) {  // 'this' is the form element
    var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
        replaceAttrValue = container.attr("data-valmsg-replace"),
        replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;

    container.removeClass("field-validation-valid").addClass("field-validation-error");
    error.data("unobtrusiveContainer", container);

    if (replace) {
        container.empty();
        error.removeClass("input-validation-error").appendTo(container);
    }
    else {
        error.hide();
    }
    //Call custom function to overwrite validation messages
    if (typeof setValidationMessages == 'function')
        setValidationMessages(inputElement[0]);
}

The "setValidationMesages" function is in the baseformhelper.js. If it is not available it won't break jquery.validate, but if it is, it will call the validation for each form field added to the presentation. Using the same "if function available, call it" logic.

//Base Form Helper
//Perform important form related functions and calculations here...

//Attempt to call function responsible for updating validation message with value from sitecore for each form field 
//if not available, nothing will break
function setValidationMessages(element) {
switch (element.id)
{
    case "txtFirstName":
        if (typeof FirstNameValidationMsg == 'function') {
            FirstNameValidationMsg();
        }
        break;
    case "txtLastName":
        if (typeof LastNameValidationMsg == 'function') {
            LastNameValidationMsg();
        }
        break;
...

So in sitecore, I can add my dynamic fields to my placeholder and they will update the Contact Model with their value, while displaying their unique validation message. When I post to my controller, all the values for the fields I decided to add to the presentation are present and I can do with them what I want. Yay, problem solved. Until next time...

public class ContactFormController : Controller
    {
        [HttpPost]
        public ActionResult SubmitContact(Models.Contact postedContact)
        {            
            if(ModelState.IsValid)
            {
                //Do something like call to an API to post to Eloqua, CRM, or update Sitecore Contact for DMS/Experience Reports
            }
            else
            {
                //Do something else like log information about the contact so you know who tried to fill out your form even though your form submission logic is screwed up.
            }
        }
...