Make controller return a part dynamically to any page that has the widget when !ModelState.IsValid

Topics: Writing modules
Feb 28, 2012 at 2:55 PM
Edited Feb 28, 2012 at 2:56 PM

So I've made an orchard module that is basically a contact form as a widget. This widget can be placed on any page as a part(or as a widget) and it posts it's data to a controller that handles the post data, once it sends the email it returns a "sent" view which summarizes their input. The issue I'm having is handling an invalid model. As I debug the code, it steps into the validation just fine.

if (!ModelState.IsValid)
{
   
//Gets here no problem
}

But the problem is that since my widget can basically be on any page (home page, on a separate page, in a global area so its on every page), it can be a widget or part of a type. I don't know how to send the model back to the sender page. I'm thinking I have a few options.

An interesting side note is that if I put my form in a "global" area of my site( as in a side bar that's on every page, more importantly my "sent" page) then the form on that page will actually have the "validation" errors render on the form. So somehow it's still passing my view-model to a page even thought I don't explicitly return it.

a) Somehow map the sender url to a controller action, for example if the url has no subdomain, then we route it to a homepage controller or whatever (not sure how orchard handles it)

b) Somehow do it in the handler/driver area, also not sure how I'd do this.

c) Rather not do it this way but just make my controller return json data and make the form post to the controller async via jquery.

I don't know if I explained my situation well, but if not please ask me to clarify, if you need code samples I can post those as well.

Feb 28, 2012 at 3:17 PM

I know exactly what you're talking about, I've been contemplating the same problem in something I'm working on.

This is the big thing I miss about WebForms, it didn't matter then because you were always posting back to the same page anyway ...

Anyway, the solution I think I'm going with has two parts:

1) Implement as much validation client-side / AJAX as possible

2) Post to a controller which also performs server-side validation and displays its own copy of the form in case of an error (or a success page otherwise)

The only disadvantage here is that if they have JS disabled and there are errors, they are transferred to a different page. But I don't think that's too bad.

The validation errors you're seeing might be through the notifications system, otherwise I'm not sure how they're getting there.

Feb 28, 2012 at 10:38 PM

I'm also interested in solutions/approaches to this. I have some widgets that are forms too, that show up on many of my pages, and so far made them work via ajax. It's tough to learn MVC and Orchard at the same time, and this was one of the pain points. 

Developer
Feb 28, 2012 at 10:57 PM
Edited Feb 28, 2012 at 10:59 PM

Although it's not a solution that you can work with right now, but what I think would be nice here is to be able to have an extra method on the driver which is very much like the Editor method that updates the model. You see, when Orchard invokes Editor via IContentManager.UpdateEditor, the Editor method gets called, where you can perform model validation. Any validation errors will be displayed on the page. This is how it works in the backend.

For the front end, we should be able to post back not to just any controller, but perhaps to ItemController (of the Routable module). Currently, it has a Display action method. Perhaps it could be extended to include a Postback action method, which will invoke IContentManager.UpdateDisplay (as opposed to BuildDisplay). The driver of your widget could then validate its (view)model or part, add any modelstate errors, which gets then picked up by the template that renders the widget.

Perhaps you could already do it like this if you replace the entire Routable module with one of your own, and if UpdateEditor supported the DisplayType argument, like BuildDisplay does (but alas it doesn't as of yet; perhaps in 1.4 things have changed).

Another approach that might work right away is that you post the current url as part of the form values to your controller, and rebuild the entire display from there. To know what content item you're rendering, you use the posted url to query the content manager which content item (with the RoutablePart) has the specified slug.

I haven't tried this, but I think it could work.

Feb 25, 2013 at 8:40 AM
Have this been implemented yet? A postback function for Display in the driver... :) I really really need this or another way to implement a custom form.
Feb 25, 2013 at 12:56 PM
Edited Feb 25, 2013 at 12:58 PM
I had to implement a form widget this way last fall. Ultimately what I did was create a custom widget:
  • View - passing VM data + the returnUrl to Controller Action
  • ViewModel - decorated properties with Validation attributes
  • Controller
    • If AJAX - returned a JsonResponse complex type as Json that housed a list of errors (from ModelState), a custom Message, a Status, etc
      • View - if "Errors" was not null, I display the errors as a list - mimicking a validation Summary
    • If Not AJAX - throwing Errors, Message, Status, etc into TempData and doing "RedirectLocal" with the returnUrl
      • View - if there are TempData "Errors", I display the errors as a list - mimicking a validation Summary
I can share an image or more details if you're interested in this approach. It's not CMSy - which I don't like - but it got the job done at the end of the day.
Feb 26, 2013 at 9:53 AM
Hello.

I've used TempData to solve this problem when i've embedded forms in my MVC3/4 Layouts on different pages. I am familiar with the solution. I am just not sure how to implement it in Orchard. I'd like to know more in detail if you have the time? :) Code snippets also help if you have any.

Best regards,
Andreas
Feb 26, 2013 at 12:03 PM
Andreas,

I'd be happy to share my approach. I have a fully-functioning widgetized module. Let me pull some code snippets, and I'll post them here.
Feb 26, 2013 at 12:22 PM
That would be awesome. Thanks! :)

/Andreas
Feb 27, 2013 at 8:59 AM
Hello.

While waiting for your code snippets i tried to implement my own solution using TempData. I stored errors in TempData, but when i checked TempData for errors is was empty. It seems like TempData didnt "survive" the redirect.

/Andreas
Feb 27, 2013 at 2:56 PM
Status update.

I've managed to build a solution using Notifier in IOrchardServices. I create one or more errors and redirect to the returnUrl. It works well in cases where the form is displayed first on the page (since the Message zone is located at the top of the page). Still looking forward to seeing your approach, though. I am sure its better than mine. I am still an Orchard noob :)

/Andreas
Feb 27, 2013 at 8:55 PM
Here's my View/Part:
@using Example.SelfContainedWidget.Models
@using Example.SelfContainedWidget.ViewModels
@{
    var submitForm = (SelfContainedWidgetViewModel)Model.SelfContainedWidgetViewModel;

    if (submitForm == null)
    {
        submitForm = new SelfContainedWidgetViewModel()
        {
            Name = Convert.ToString(TempData["SubmitFormName"]),
            Email = Convert.ToString(TempData["SubmitFormEmail"])
        };
    }

    IList<string> validationErrors = new List<string>();

    if (TempData["SubmitFormValidationErrors"] != null)
    {
        validationErrors = TempData["SubmitFormValidationErrors"] as List<string>;
    }

    string returnUrl = Request.RawUrl;

    Script.Require("jQuery").AtHead();
    Script.Require("SubmitFormFunctions").AtHead();

    Style.Require("SubmitFormStyle").AtHead();
}

@if (validationErrors.Count > 0)
{
<div id="submitFormValidationSummary" class="validation-summary-errors" >
    <ul id="submitFormList">
    @foreach (var error in validationErrors)
    {
        <li>@error</li>
    }
    </ul>
</div>
}
else
{
<div id="submitFormValidationSummary" class="validation-summary-errors" style='display: none'>
    <ul id="submitFormList">
    </ul>
</div>    
}

<article id="self-contained-widget-article">
    <div class="self-contained-widget-content">
        <p>
            Sign up below!
        </p>

        @using (Html.BeginFormAntiForgeryPost(Url.Action("Signup", "SelfContainedWidget", new { area = "Example.SelfContainedWidget" }), FormMethod.Post, new { id = "submitForm" }))
        {
            
            <input id="__requesttoken" name="__requesttoken" type="hidden" value="@Html.AntiForgeryTokenValueOrchard()" />
            <input id="returnUrl" name="returnUrl" type="hidden" value="@returnUrl" />
            
            <ul>
                <li>
                    <div>@Html.LabelFor(m => submitForm.Name, T("Name"))</div>
                    <div>@Html.TextBoxFor(m => submitForm.Name)</div>     
                </li>

                <li>
                    <div>@Html.LabelFor(m => submitForm.Email, T("Email"))</div>
                    <div">@Html.TextBoxFor(m => submitForm.Email)</div>   
                </li>
            </ul>

            <input id="btnSubmitForm" type="submit" value="@T("Subscribe")" />
        }

    </div>
</article>
Feb 27, 2013 at 8:56 PM
ViewModel:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Example.SelfContainedWidget.ViewModels
{
    public class SelfContainedWidgetViewModel : IValidatableObject
    {
        [StringLength(120), Required(ErrorMessage = "Name is required"), Display(Name = "Name")]
        public string Name { get; set; }

        [StringLength(255), Required(ErrorMessage = "Email is required"), DataType(DataType.EmailAddress), Display(Name = "Email")]
        public string Email { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var results = new List<ValidationResult>();

            return results;
        }

        public void EmailSignupRequest()
        {
            // Send Email
        }

        private string BuildEmailBody()
        {
            // Build Email Body
            return string.Empty;
        }
    }
}
Feb 27, 2013 at 8:56 PM
Controller:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using Example.SelfContainedWidget.ViewModels;
using Orchard.Mvc.Extensions;
using Orchard.Themes;

namespace Example.SelfContainedWidget.Controllers
{
    public enum Status
    {
        Ok,
        Error
    }

    public class JsonResponse
    {
        public Status Status { get; set; }
        public string Message { get; set; }
        public string RedirectUrl { get; set; }
        public List<string> Errors { get; set; }
    }

    [Themed]
    public class SelfContainedWidgetController : Controller
    {

        [HttpPost]
        public ActionResult Signup(SelfContainedWidgetViewModel submitForm, string returnUrl)
        {
            JsonResponse response = new JsonResponse();

            if (Request.IsAjaxRequest())
            {
                if (!ModelState.IsValid)
                {
                    response.Status = Status.Error;
                    response.Errors = GetModelStateErrorsAsString(ModelState);
                    response.Message = "An error has occurred. Please check the list of errors.";
                }
                else
                {
                    //  Do Extra Work Here

                    //  Return Success
                    response.Status = Status.Ok;
                    response.Errors = null;
                    response.Message = "Thank you for subscribing";   
                }

                return Json(response);
            }
            else
            {
                if (ModelState.IsValid) {
                    // Do Extra Work Here
                }
                
                TempData["SubmitFormName"] = submitForm.Name;
                TempData["SubmitFormEmail"] = submitForm.Email;
                TempData["SubmitFormValidationErrors"] = this.GetModelStateErrorsAsString(ModelState);
                
                return this.RedirectLocal(returnUrl);
            }
        }

        private List<string> GetModelStateErrorsAsString(ModelStateDictionary state)
        {
            List<string> errors = new List<string>();

            foreach (var key in state.Keys)
            {
                var error = state[key].Errors.FirstOrDefault();
                if (error != null)
                {
                    errors.Add(error.ErrorMessage);
                }
            }

            return errors;
        }
    }
}
Feb 27, 2013 at 9:02 PM
Script:
function hideFormResults() {
    $('#SubmitFormValidationSummary').hide();
    $('#SubmitFormList').empty();
}

function handleSubmitFormSuccess(result) {
    $('#btnSubmitForm').removeAttr('disabled');

    if (result.Errors != null) {
        displaySubmitFormErrors(result);
    }
}

function displaySubmitFormErrors(result) {
    $('#SubmitFormValidationSummary').show();
    $.each(result.Errors, function (i, error) {
        $('#SubmitFormList').append('<li>' + error + '</li>');
    });
}

function clearForm($form) {
    $form.find("input[type=text], input[type=email], input[type=password], textarea").val("");
}

function submitForm() {
    $('#btnSubmitForm').attr('disabled', 'disabled');

    hideFormResults();
    var form = $('.newsletter-widget-content form');
    var data = form.serialize();

    $.ajax({
        url: form.attr('action'),
        data: data,
        type: 'post',
        success: function (result) {
            handleSubmitFormSuccess(result);
        },
        error: function (xhr, status, error) {
            alert('error: ' + xhr.responseText);
            $('#btnSubmitForm').removeAttr('disabled');
        }
    });

    return false;
}

$(document).ready(function () {
    $('#btnSubmitForm').click(submitForm);
});
Feb 27, 2013 at 9:03 PM
Edited Mar 1, 2013 at 12:02 PM
I had to simplify the code a lot so that you didn't have to wade as much. I may have renamed or copy/pasted incorrectly,
but hopefully you get the gist.
Mar 4, 2013 at 8:18 AM
Thanks, these code snippets help :)

/Andreas