Skip to content

Instantly share code, notes, and snippets.

@danielgreen
Last active January 15, 2018 08:36
Show Gist options
  • Save danielgreen/5669903 to your computer and use it in GitHub Desktop.
Save danielgreen/5669903 to your computer and use it in GitHub Desktop.
ASP.NET MVC does not help you to display validation errors, perform browser redirects, or update the UI if the browser postback used Ajax instead of a normal form post. The following code deals with these situations using Knockout. Use ajax_ProcessSuccess as the OnSuccess callback for Ajax.BeginForm (see http://bradwilson.typepad.com/blog/2010/1…
window.ajax_ProcessSuccess = (function ()
{
function getValidationSummary(form) {
// Look for a Validation Summary control within the form. If one does not exist, create it.
var $summ = $(form).find('*[data-valmsg-summary="true"]');
if ($summ.length == 0)
{
// Could not find a Validation Summary, so append one to the form
$summ = $('<div class="validation-summary-valid" data-valmsg-summary="true"><ul></ul></div>');
$summ.appendTo(form);
}
return $summ;
}
function getValidationList(summary) {
// Look for a list within the Validation Summary. If one does not exist, create it.
var $list = $(summary).children('ul');
if ($list.length == 0) {
$list = $('<ul></ul>');
$list.appendTo(summary);
}
return $list;
}
function getResponseModel(data) {
// Does the response contain any model (validation) errors?
if (data && data.Model && $(data.Model).length > 0)
return data.Model;
return null;
}
function UpdateModel(data, form)
{
var model = getResponseModel(data);
var viewModel = $(form).data('ViewModel');
if (!model || !ko || !viewModel)
return false;
// Update the viewModel from the response model. Knockout will update any form fields bound to viewModel.
ko.mapping.fromJS(model, viewModel);
// The page has been refreshed from the server, dismiss any warning about unsaved changes.
if (setConfirmUnload)
setConfirmUnload(false);
return true;
}
function getResponseValidationErrors(data) {
// Does the response contain any model (validation) errors?
if (data && data.ModelErrors && data.ModelErrors.length > 0)
return data.ModelErrors;
return null;
}
function CheckValidationErrorResponse(data, form, summaryElement) {
var errors = getResponseValidationErrors(data);
var $summ = summaryElement || getValidationSummary(form);
var $list = getValidationList($summ);
// Clear the error list within the Validation Summary
$list.html('');
// Mark form fields (and their validation messages) as valid to begin with
$(form).find(".input-validation-error")
.removeClass("input-validation-error");
$(form).find(".field-validation-error")
.removeClass("field-validation-error")
.addClass("field-validation-valid");
if (!errors)
{
// No validation errors were found in the response, mark the Validation Summary as valid and exit
$summ.removeClass('validation-summary-errors').addClass('validation-summary-valid');
return false;
}
// For each validation error in the response...
$.each(errors, function (i, item) {
var $val, $input, errorList = "";
if (item.Name) {
// Mark the field's validation message as denoting an error
$val = $(form).find(".field-validation-valid, .field-validation-error")
.filter("[data-valmsg-for=" + item.Name + "]")
.removeClass("field-validation-valid")
.addClass("field-validation-error");
$input = $(form).find("*[name='" + item.Name + "']");
// If no validation message exists for the field, and the field is not hidden, create a validation message
if (!$input.is(":hidden") && !$val.length)
{
$input.parent().append("<span class='field-validation-error' data-valmsg-for='" + item.Name + "' data-valmsg-replace='false'>*</span>");
}
// Mark the form field as invalid
$input.removeClass("valid").addClass("input-validation-error");
}
// Populate the error list within the Validation Summary
$.each(item.Errors, function (c, err) {
errorList += "<li>" + err + "</li>";
});
$list.append(errorList);
});
// Mark the Validation Summary as containing errors
$summ.removeClass('validation-summary-valid').addClass('validation-summary-errors');
return true;
}
return function (data, status, xhr, form, successMessageOrCallback) {
if (data) {
if (data.Redirect) {
location.href = data.Redirect;
return;
}
var form = $('#' + formId);
UpdateModel(data, form);
CheckValidationErrorResponse(data, form);
window.UnblockPage();
if (!successMessageOrCallback)
return;
if (typeof successMessageOrCallback == 'function') {
successMessageOrCallback(data);
}
else {
window.BlockWithMessage(successMessageOrCallback);
}
}
};
})();
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Web.Json;
namespace Web.Filters
{
/// <summary>
/// When applied to an action method, this filter converts Redirect and View results to JSON.
/// For a Redirect, the JSON contains the target URL. For a View result, the JSON contains the model and any model state errors.
/// </summary>
public class AjaxResultAttribute : ActionFilterAttribute
{
private const string CONTENT_TYPE_APPLICATION_JSON = "application/json";
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (!filterContext.HttpContext.Request.IsAjaxRequest())
return;
var isRedirect = filterContext.Result is RedirectResult || filterContext.Result is RedirectToRouteResult;
var isView = filterContext.Result is ViewResultBase;
if (isRedirect)
{
var url = "/";
if (filterContext.Result is RedirectResult)
{
var result = filterContext.Result as RedirectResult;
url = UrlHelper.GenerateContentUrl(result.Url, filterContext.HttpContext);
}
else
{
var result = filterContext.Result as RedirectToRouteResult;
url = UrlHelper.GenerateUrl(result.RouteName, null, null, result.RouteValues, RouteTable.Routes, filterContext.RequestContext, false);
}
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
filterContext.Result = new JsonResult
{
Data = new { Redirect = url },
ContentEncoding = System.Text.Encoding.UTF8,
ContentType = CONTENT_TYPE_APPLICATION_JSON,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else if (isView)
{
var oldResult = filterContext.Result as ViewResultBase;
ModelStateDictionary modelState = oldResult.ViewData.ModelState;
dynamic resultData;
if (modelState.IsValid)
{
resultData = new { Model = oldResult.Model };
}
else
{
resultData = new
{
Model = oldResult.Model,
ModelErrors = from ms in modelState
let ec = ms.Value.Errors
where ec.Count > 0
select new
{
Name = ms.Key,
Errors = ec.Select(e => e.ErrorMessage).Union(
ec.Where(e => e.Exception != null).Select(e => e.Exception.Message))
}
};
}
filterContext.Result = new JsonDotNetResult
{
Data = resultData,
ContentEncoding = System.Text.Encoding.UTF8,
ContentType = CONTENT_TYPE_APPLICATION_JSON,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
// Just return the result to the client unchanged
}
}
}
}
namespace Web.Controllers
{
/// <summary>
/// base controller class which contains some common functionality
/// </summary>
public abstract class BaseController : Controller
{
protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
{
return new JsonDotNetResult
{
Data = data,
ContentType = contentType,
ContentEncoding = contentEncoding,
JsonRequestBehavior = behavior
};
}
}
}
window.BlockPage = function () {
if ($.blockUI)
$.blockUI({ baseZ: 2000 }); // ensure the blocker appears in front of other elements
};
window.UnblockPage = function () {
if ($.unblockUI)
$.unblockUI();
};
// Display a modal dialog with the error message
window.BlockWithMessage = function (messageText, heading, popupClass) {
if ($.blockUI) {
var $popup = $('<div class="' + (popupClass || 'MessagePopup') + '"><h1>' + (heading || 'Success') + ' </h1><p>' + messageText + '</p><button>OK</button></div>');
$popup.find('button').click(window.UnblockPage);
$.blockUI({ message: $popup, baseZ: 2000 });
}
else {
alert(heading + ': ' + messageText);
}
};
// Display a modal dialog with the error message
window.ajax_ProcessError = function (xhr, status, error) {
window.BlockWithMessage(xhr.responseText, 'Error', 'ErrorPopup');
};
protected void Application_Start()
{
SetupJsonProvider();
}
// Use the JSON.NET library to deserialize incoming JSON data
// https://gist.github.com/DalSoft/1588818
private void SetupJsonProvider()
{
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new JsonDotNetValueProviderFactory());
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
namespace Web.Json
{
public class JsonDotNetResult : JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.");
}
var response = context.HttpContext.Response;
response.ContentType = !String.IsNullOrEmpty(ContentType) ? ContentType : "application/json";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
if (Data == null)
return;
// If you need special handling, you can call another form of SerializeObject below
var serializedObject = JsonConvert.SerializeObject(Data, Formatting.Indented);
response.Write(serializedObject);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Dynamic;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Web.Json
{
public sealed class JsonDotNetValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;
var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
var bodyText = reader.ReadToEnd();
return String.IsNullOrEmpty(bodyText) ? null : new DictionaryValueProvider<object>(JsonConvert.DeserializeObject<ExpandoObject>(bodyText, new ExpandoObjectConverter()) , CultureInfo.CurrentCulture);
}
}
}
@using (Ajax.BeginForm("Edit", "Events", FormMethod.Post, new AjaxOptions { OnFailure = "ajax_ProcessError", OnSuccess = "ajax_ProcessSuccess(data, status, xhr, 'eventForm', ShowSaveSuccess);" }, new { id = "eventForm" }))
{
@Html.HiddenFor(model => model.ID)
@Html.HiddenFor(model => model.LastEditedOn, new { data_bind = "value: LastEditedOn" })
@Html.DropDownListFor(a => a.StatusID, Model.StatusData,
new { @class = "dropdown",
data_bind = "options: StatusData, optionsText: 'Text', optionsValue: 'Value', value: StatusID" })
}
<script type="text/javascript">
$(function () {
// Serialize the Model using Json.NET and use Knockout to map it to a ViewModel
var viewModel = ko.mapping.fromJSON('@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model))');
$('#eventForm').data('ViewModel', viewModel);
ko.applyBindings(viewModel);
// Dismiss spurious warnings about unsaved changes if the user tries to leave the page.
// Knockout may have caused these when applying its bindings.
if (setConfirmUnload)
setConfirmUnload(false);
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment