I'm upgrading from sitecore 7.2 to sitecore 8.1. I have 5 years of experience with Web Forms but only a few months of exposure to MVC. In addition to upgrading to 8.1 my boss wants to change from Web Forms to MVC. My company has somewhat complicated form logic in Sitecore with Asp.Net Web Forms where each form field is its own Sublayout(User Control). This allows the content editor to include, not include, and reorder form fields based on the requirements given by the business. Here's how I accomplish this in Web Forms (BTW we also use GlassMapper): Form Markup Code Snippet:
<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.
Next is the Markup for one of the basic field sublayouts. I'll use 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.
OK so last week I figured out an answer to this question, and I didn't want to post it until I had time to explain thoroughly. So like I stated in the question, our forms are just sitecore placeholders and we reuse the same form field user controls (sublayouts) in all of our forms. So in MVC I want something similar. So you see in this form example, we have only one placeholder like the WebForms version. *Notice the reference to baseformhelper.js for later.
@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.
}
}
...