I am using ASP.NET MVC3 for a form that has both server and client validations. I'm showing error messages as balloons above the inputs. Due to the presentation of the errors, I need to only show one error at a time, otherwise the balloons tend to obscure other fields that may also be in error.
How can I customize the validation behavior to only render the first error message?
Edit: Please notice that the form has both server and client validations, and that I only want to show the first error message for the entire form (not per field).
In case anyone needs it, the solution I came up with is to add the following script towards the bottom of the page. This hooks into the existing javascript validation to dynamically hide all but the first error in the form.
<script>
$(function() {
var form = $('form')[0];
var settings = $.data(form, 'validator').settings;
var errorPlacementFunction = settings.errorPlacement;
var successFunction = settings.success;
settings.errorPlacement = function(error, inputElement) {
errorPlacementFunction(error, inputElement);
showOneError();
}
settings.success = function (error) {
successFunction(error);
showOneError();
}
function showOneError() {
var $errors = $(form).find(".field-validation-error");
$errors.slice(1).hide();
$errors.filter(":first:not(:visible)").show();
}
});
</script>
Could give this a shot on your controller action
var goodErrors = ModelState.GroupBy(MS => MS.Key).Select(ms => ms.First()).ToDictionary(ms => ms.Key, ms => ms.Value);
ModelState.Clear();
foreach (var item in goodErrors)
{
ModelState.Add(item.Key, item.Value);
}
I'm just selecting only one of each property error, clearing all errors then adding the individual ones back.
this is completely untested but should work.
You could create a custom validation summary which would display only the first error. This could be done either by creating an extension for the HtmlHelper class, or by writing your own HtmlHelper. The former is the more straightforward.
public static class HtmlHelperExtensions
{
static string SingleMessageValidationSummary(this HtmlHelper helper, string validationMessage="")
{
string retVal = "";
if (helper.ViewData.ModelState.IsValid)
return "";
retVal += #"<div class=""notification-warnings""><span>";
if (!String.IsNullOrEmpty(validationMessage))
retVal += validationMessage;
retVal += "</span>";
retVal += #"<div class=""text"">";
foreach (var key in helper.ViewData.ModelState.Keys)
{
foreach(var err in helper.ViewData.ModelState[key].Errors)
retVal += "<p>" + err.ErrorMessage + "</p>";
break;
}
retVal += "</div></div>";
return retVal.ToString();
}
}
This is for the ValidationSummary, but the same can be done for ValidationMessageFor.
See: Custom ValidationSummary template Asp.net MVC 3
Edit: Client Side...
Update jquery.validate.unobstrusive.js. In particular the onError function, where it says error.removeClass("input-validation-error").appendTo(container);
Untested, but change that line to: error.removeClass("input-validation-error").eq(0).appendTo(container);
Create a html helper extension that renders only one message.
public static MvcHtmlString ValidationError(this HtmlHelper helper)
{
var result = new StringBuilder();
var tag = new TagBuilder("div");
tag.AddCssClass("validation-summary-errors");
var firstError = helper.ViewData.ModelState.SelectMany(k => k.Value.Errors).FirstOrDefault();
if (firstError != null)
{
tag.InnerHtml = firstError.ErrorMessage;
}
result.Append(tag.ToString());
return MvcHtmlString.Create(result.ToString());
}
Update the jquery.validate.unobtrusive.js OnErrors function as below,
function onErrors(form, validator) { // 'this' is the form element
// newly added condition
if ($(form.currentTarget).hasClass("one-error")) {
var container = $(this).find(".validation-summary-errors");
var firstError = validator.errorList[0];
$(container).html(firstError.message);
}
else {
var container = $(this).find("[data-valmsg-summary=true]"),
list = container.find("ul");
if (list && list.length && validator.errorList.length) {
list.empty();
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");
$.each(validator.errorList, function () {
$("<li />").html(this.message).appendTo(list);
});
}
}
}
Basically we have added a condition in the OnError to check whether the form contains a css-class named one-error and if yes then displays a single error else display all.
Related
I have a kendo upload control like this:
#(Html.Kendo().Upload()
.Name("attachments")
.Async(a => a
.Save("UploadAsync", "Intel")
.Remove("RemoveAsync", "Intel")
.AutoUpload(true)
)
.Events(e => e
.Success("onSuccessfulUpload")
.Remove("onRemoveFile")
)
.Validation(v => v.AllowedExtensions(exts))
)
In the controller, its Save method is like this:
public ActionResult UploadAsync(IEnumerable<HttpPostedFileBase> attachments)
{
string filename;
// ... do things ...
return Json(new { ImageName = filename }, "text/plain");
}
where the variable filename is assigned a value.
Its Remove method in the controller looks very similar:
public ActionResult RemoveAsync(string[] fileNames)
{
string filename;
// ... do things ...
return Json(new { ImageName = filename }, "text/plain");
}
I verified that both controller methods are called correctly and the variable filename is assigned to in both cases.
The upload works as expected, and the Success event also works as expected. (The alert is simply for testing.)
function onSuccessfulUpload(e) {
alert(e.response.ImageName);
}
The issue comes on removal of a file.
When I get to the Remove event, e does not have a .response. It has e.files and e.sender, but no response.
function onRemoveFile(e) {
alert(e.response); // undefined!
alert(JSON.stringify(e.files)); // works, but does not have what I need
}
How do I access what the RemoveAsync method returns?
It looks like the remove event doesn't provide this kind of data, so I see only a workaround to solve this.
You could try to put the result name to the headers, and you should be able to read the result:
// Controller
Response.AddHeader("ImageName", imageName); // before Json(...)
// View/JS
alert(e.headers['ImageName']);
I haven't tested that and I see a risk that that the remove event doesn't really read the async response, that would explain why the response object is not available.
In that case, you could try to use the following workaround: Don't call any Url on remove (or use some Action without any body, just a plain result) and inside of the event callback, execute RemoveAsync yourself.
// View/JS
function onRemoveFile(e) {
$.post('#Html.Url("RemoveAsync", "Intel")', e.files, function(response) {
alert(response);
});
}
It's not pretty, but it should work and provide the results you need.
After some time poking around, I found the answer.
The key lay in the order of the events. My first assumption was that the Success event was called after successful upload, and the Remove event was called after successful(?) removal. This was wrong.
The actual order of the events is:
JS onUpload > Controller UploadAsync > JS onSuccess
JS onRemoveFile > Controller RemoveAsync > JS onSuccess
My Solution:
I created two parallel arrays in javascript to represent the files uploaded in the client-side e.files, which contains uid's for each file, and the filenames created by the server-side controller method (which renames the files).
var fileUids = [];
var fileSaveNames = [];
I changed the onSuccessfulUpload function to this, when I discovered that there is an e.operation that specifies which operation was the successful one:
function onSuccess(e) {
if (e.operation == "upload") {
var filename = e.response.ImageName;
var uid = e.files[0].uid;
// add to the arrays
fileUids.push(uid);
fileSaveNames.push(filename)
// ...
}
else if (e.operation == "remove") {
var uid = e.files[0].uid;
var saveIdx = fileUids.indexOf(uid);
// remove from the arrays
fileSaveNames.splice(saveIdx, 1);
fileUids.splice(saveIdx, 1);
// ...
}
}
Then I updated the removeFile function, which I now knew was called before the method in the controller.
function removeFile(e) {
var uid = e.files[0].uid;
var idx = fileUids.indexOf(uid);
e.data = { fileToRemove: fileSaveNames[idx] };
}
That last line, where I assign to e.data, was because of this thread on the Telerik forums, which has the following info:
Solution: All that's needed it to define a function for the upload
event and modify the "data" payload.
Add the upload JS function to add a parameter "codeID" in my case.
$("#files").kendoUpload({
[...]
upload: function (e) {
e.data = { codeID: $("#id").val() };
}
});
Now on the controller add the parameter and that's it.
[HttpPost]
public ActionResult Save(IEnumerable<HttpPostedFileBase> files, Guid codeID) { }
(Instead of being in the Upload event, mine is in the Remove event.)
I chose the parameter name fileToRemove, and now the new RemoveAsync method in the controller is as such:
public ActionResult RemoveAsync(string[] fileNames, string fileToRemove)
{
string returnName = "";
if (!string.IsNullOrWhiteSpace(fileToRemove))
{
// ... do things ...
}
return Json(new { ImageName = returnName }, "text/plain");
}
I just started working with ASP MVC using DevExpress, I've created views that have GridViews inside, and I had the settings in the partial view.
Short story
I need to have the GridViewSettings object inside a controller rather than the view.
Why? Because the app needs to download an Excel file of the grid and so far this is the only approach I've seen. But the problem is, having the grid settings in the controller doesn't allow me to specify a label inside an unbound column.
Here's the code:
public GridViewSettings MyGridSettings()
{
var settings = new GridViewSettings();
settings.Name = "MyGrid";
settings.CommandColumn.Visible = true;
settings.KeyFieldName = "PERSON_ID";
settings.SettingsPager.Visible = true;
settings.Settings.ShowGroupPanel = true;
settings.Settings.ShowFilterRow = true;
settings.SettingsBehavior.AllowSelectByRowClick = true;
settings.SettingsAdaptivity.AdaptivityMode = GridViewAdaptivityMode.HideDataCellsWindowLimit;
settings.SettingsAdaptivity.AdaptiveColumnPosition = GridViewAdaptiveColumnPosition.Right;
settings.SettingsAdaptivity.AdaptiveDetailColumnCount = 1;
settings.SettingsAdaptivity.AllowOnlyOneAdaptiveDetailExpanded = false;
settings.SettingsAdaptivity.HideDataCellsAtWindowInnerWidth = 0;
settings.Columns.Add("PERSON_ID", "Person ID");
settings.Columns.Add(col =>
{
col.Caption = "Department";
col.SetDataItemTemplateContent(dataTemplate =>
{
String DepartmenID = (String)DataBinder.Eval(dataTemplate.DataItem, "DEPARTMENT_ID");
//if (DepartmenID != null)
//{
// Html.DevExpress().Label(label =>
// {
// label.Text = String.Format("{0}",
// DataBinder.Eval(dataTemplate.DataItem, "DEPARTMENT.NAME"));
// }).Render();
// }
}
}
}
Short Question
How do I access the following code from a Controller?
Html.DevExpress().Label(label =>
{
label.Text = String.Format("{0}",
DataBinder.Eval(dataTemplate.DataItem, "DEPARTMENT.NAME"));
}).Render();
Like I said I need to do this because the Excel File Download. Although, if you have any workarounds where I don't need to do this that'd be great
Using HtmlHelper class described here (read Using HtmlHelper class section), probably this code may solve your issue:
public GridViewSettings MyGridSettings(this HtmlHelper html)
{
var settings = new GridViewSettings();
settings.Name = "MyGrid";
// simplified for brevity
settings.Columns.Add(col =>
{
col.Caption = "Department";
col.SetDataItemTemplateContent(dataTemplate =>
{
String DepartmentID = (String)DataBinder.Eval(dataTemplate.DataItem, "DEPARTMENT_ID");
if (DepartmentID != null)
{
html.DevExpress().Label(label =>
{
label.Text = String.Format("{0}", DataBinder.Eval(dataTemplate.DataItem, "DEPARTMENT.NAME"));
}).Render();
}
}
}
return settings;
}
Unlike views, controller actions doesn't have reference to HtmlHelper by default, hence you need to include HtmlHelper class inside GridViewSettings method to create HTML helper extensions.
In case your DataItemTemplateContent can't be exported by design, try exporting GridView contents to XtraReport using How to convert and then print an GridView extension by using the XtraReport example.
Additional references:
Grid View Exporting - 1
Grid View Exporting - 2
I have a scenario where 1 of 10 fields needs to be completed. When I add an error to each of the 10 properties, this results in the same error message appearing in the validation summary 10 times.
I have looked at this ValidationSummary displays duplicate messages
public static MvcHtmlString UniqueValidationSummary(this HtmlHelper html, bool excludePropertyErrors)
{
// do some filtering on html.ViewData.ModelState
return System.Web.Mvc.Html.ValidationExtensions.ValidationSummary(html, excludePropertyErrors);
}
But I am not sure how to actually get it working. When the extension function is run on page load html.ViewData.ModelState is valid and has no messages.
How can I strip out any duplicate error messages via this extension?
You have to write helper method that the following code.
public static IHtmlString UniqueValidationSummary(ModelStateDictionary ms)
{
var resultHtml = new StringBuilder();
resultHtml.Append("<div class=\"validation-summary-errors text-danger\" data-valmsg-summary=\"true\">");
resultHtml.Append("<ul>");
var isError = false;
var knownValues = new HashSet<string>();
foreach (var key in ms.Keys)
{
foreach (var e in ms[key].Errors)
{
isError = true;
if (!knownValues.Contains(e.ErrorMessage))
{
resultHtml.Append("<li>" + e.ErrorMessage + "</li>");
knownValues.Add(e.ErrorMessage);
}
}
}
if (!isError) return null;
resultHtml.Append("</ul>");
resultHtml.Append("</div>");
return new HtmlString(resultHtml.ToString());
}
And then, you can use helper method from view(.cshtml).
#MyHelper.UniqueValidationSummary(ViewData.ModelState);
I wouldn't usually recommend doing this but in some cases it might be needed.
I recently ran into a similar problem and needed to do the same.
Instead of trying to parse the ModelState in razor view, i did it in the controller, before returning the view. Here is the extension i used:
(Please Note that this hasnt been extensively tested, but seems to be working - i just wrote it)
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
namespace WebApplication.Common
{
public static class ModelStateExtension
{
public static void RemoveDuplicateErrorMessages(this ModelStateDictionary modelStateDictionary)
{
//Stores the error messages we have seen
var knownValues = new HashSet<string>();
//Create a copy of the modelstatedictionary so we can modify the original.
var modelStateDictionaryCopy = modelStateDictionary.ToDictionary(
element => element.Key,
element => element.Value);
foreach (var modelState in modelStateDictionaryCopy)
{
var modelErrorCollection = modelState.Value.Errors;
for(var i = 0 ; i < modelErrorCollection.Count ; i++)
{
//Check if we have seen the error message before by trying to add it to the HashSet
if (!knownValues.Add(modelErrorCollection[i].ErrorMessage))
{
modelStateDictionary[modelState.Key].Errors.RemoveAt(i);
}
}
}
}
}
}
You simple need to call the extension on your ModelState before returning the view:
using WebApplication.Common;
if (!ModelState.IsValid)
{
//viewModel code omitted
ModelState.AddModelError("0", "Server Side Validation failed");
ModelState.RemoveDuplicateErrorMessages();
return View(viewModel);
}
I have the following code:
string init = "yes";
string html = "";
foreach (var item in v.Details) {
if (item.Substring(0, 1) != " ")
{
if (init != "yes")
{
html += "</ul>";
}
html += "<p>" + item + "</p><ul>";
}
else
{
html += "<li>" + item.Substring(1) + "</li>";
}
}
The code is in my MVC controller and it creates a string called html. The thing is I don't think it should be in the controller. I tried to put this into the view and ended up with a huge mess that doesn't seem to work. Seems I am not very good at coding C within a razor view. I just saw a lot of syntax type errors and confusion between what's C and what's HTML.
Can anyone suggest how I could make this code work within a view. Here's what I used to have:
<ul>
#foreach (var item in Model.Details)
{
<li>#item</li>
}
</ul>
This worked but as you can see I now need more processing. Would it be better to take this out of the view and if so how could I do this. I'm really hoping for a view solution but I am confused about where the put the #'s and where to put the brackets.
Any experts at coding C inside or Razor out there?
The code is in my MVC controller and it creates a string called html.
The thing is I don't think it should be in the controller
You are correct. It shouldn't be in the view neither due to the absolute mess it would create. I think this code is better suited in a custom HTML helper:
public static class HtmlExtensions
{
public static IHtmlString FormatDetails(this HtmlHelper htmlHelper, IEnumerable<string> details)
{
var init = "yes";
var html = new StringBuilder();
foreach (var item in details)
{
if (item.Substring(0, 1) != " ")
{
if (init != "yes")
{
html.Append("</ul>");
}
html.AppendFormat("<p>{0}</p><ul>", htmlHelper.Encode(item));
}
else
{
html.AppendFormat("<li>{0}</li>", htmlHelper.Encode(item.Substring(1)));
}
}
return MvcHtmlString.Create(html.ToString());
}
}
which you would invoke in your view:
#Html.FormatDetails(Model.Details)
Remark: there seems to be something wrong with the init variable. You are setting its value to yes initially but you never modify it later.
I'm implementing CAPTCHA in my form submission as per Sanderson's book Pro ASP.NET MVC Framework.
The view fields are generated with:
<%= Html.Captcha("testCaptcha")%>
<%= Html.TextBox("attemptCaptcha")%>
The VerifyAndExpireSolution helper is not working as his solution is implemented.
I'm adding validation and when it fails I add a ModelState error message and send the user back to the view as stated in the book:
return ModelState.IsValid ? View("Completed", appt) : View();
But, doing so, generates a new GUID which generates new CAPTCHA text.
The problem is, however, that the CAPTCHA hidden field value and the CAPTCHA image url both retain the original GUID. So, you'll never be able to enter the correct value. You basically only have one shot to get it right.
I'm new to all of this, but it has something to do with the view retaining the values from the first page load.
Captcha is generated with:
public static string Captcha(this HtmlHelper html, string name)
{
// Pick a GUID to represent this challenge
string challengeGuid = Guid.NewGuid().ToString();
// Generate and store a random solution text
var session = html.ViewContext.HttpContext.Session;
session[SessionKeyPrefix + challengeGuid] = MakeRandomSolution();
// Render an <IMG> tag for the distorted text,
// plus a hidden field to contain the challenge GUID
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
string url = urlHelper.Action("Render", "CaptchaImage", new{challengeGuid});
return string.Format(ImgFormat, url) + html.Hidden(name, challengeGuid);
}
And then I try to validate it with:
public static bool VerifyAndExpireSolution(HttpContextBase context,
string challengeGuid,
string attemptedSolution)
{
// Immediately remove the solution from Session to prevent replay attacks
string solution = (string)context.Session[SessionKeyPrefix + challengeGuid];
context.Session.Remove(SessionKeyPrefix + challengeGuid);
return ((solution != null) && (attemptedSolution == solution));
}
What about re-building the target field names with the guid? Then, each field is unique and won't retain the previous form generations' value?
Or do I just need a different CAPTCHA implementation?
I had the same problem using the captcha example from Sanderson's book. The problem is that the page gets cached by the browser and doesn't refresh after the captcha test fails. So it always shows the same image, even though a new captcha has been generated and stored for testing.
One solution is to force the browser to refresh the page when reloading after a failed attempt; this won't happen if you just return View(). You can do this using RedirectToAction("SubmitEssay") which will hit the action method accepting HttpVerbs.Get.
Of course, you lose the ability to use ViewData to notify your user of the error, but you can just include this in the query string, then just check the query string to display your message.
So, following the book's example,
if (!CaptchaHelper.VerifyAndExpireSolution(HttpContext, captcha, captchaAttempt)
{
RedirectToAction("SubmitEssay", new { fail = 1 });
}
Then just check if the QueryString collection contains 'fail' to deliver your error message.
So, I decided to implement reCaptcha. And I've customized my view likewise:
<div id="recaptcha_image"></div>
<a href="#" onclick="Recaptcha.reload();">
generate a new image
</a><br />
<input type="text" name="recaptcha_response_field"
id="recaptcha_response_field" />
<%= Html.ValidationMessage("attemptCaptcha")%>
<script type="text/javascript"
src="http://api.recaptcha.net/challenge?k=[my public key]"></script>
This creates two captchas- one in my image container, and another created by the script. So, I added css to hide the auto-generated one:
<style type="text/css">
#recaptcha_widget_div {display:none;}
</style>
Then, in my controller, I merely have to test for captchaValid:
[CaptchaValidator]
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult SubmitEssay(Essay essay, bool acceptsTerms, bool captchaValid)
{
if (!acceptsTerms)
ModelState.AddModelError("acceptsTerms",
"You must accept the terms and conditions.");
else
{
try
{
// save/validate the essay
var errors = essay.GetRuleViolations(captchaValid);
if (errors.Count > 0)
throw new RuleException(errors);
}
catch (RuleException ex)
{
ex.CopyToModelState(ModelState, "essay");
}
}
return ModelState.IsValid ? View("Completed", essay) : View();
}
public NameValueCollection GetRuleViolations(bool captchaValid)
{
var errors = new NameValueCollection();
if (!captchaValid)
errors.Add("attemptCaptcha",
"Please enter the correct verification text before submitting.");
// continue with other fields....
}
And all of this assumes that you've implemented the Action Filter attribute and the view helper as detailed at recaptcha.net:
public class CaptchaValidatorAttribute : ActionFilterAttribute
{
private const string CHALLENGE_FIELD_KEY = "recaptcha_challenge_field";
private const string RESPONSE_FIELD_KEY = "recaptcha_response_field";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var captchaChallengeValue =
filterContext.HttpContext.Request.Form[CHALLENGE_FIELD_KEY];
var captchaResponseValue =
filterContext.HttpContext.Request.Form[RESPONSE_FIELD_KEY];
var captchaValidtor = new Recaptcha.RecaptchaValidator
{
PrivateKey = "[my private key]",
RemoteIP = filterContext.HttpContext.Request.UserHostAddress,
Challenge = captchaChallengeValue,
Response = captchaResponseValue
};
var recaptchaResponse = captchaValidtor.Validate();
// this will push the result value into a parameter in our Action
filterContext.ActionParameters["captchaValid"] = recaptchaResponse.IsValid;
base.OnActionExecuting(filterContext);
}
}
html helper:
public static class Captcha
{
public static string GenerateCaptcha( this HtmlHelper helper )
{
var captchaControl = new Recaptcha.RecaptchaControl
{
ID = "recaptcha",
Theme = "clean",
PublicKey = "[my public key]",
PrivateKey = "[ my private key ]"
};
var htmlWriter = new HtmlTextWriter( new StringWriter() );
captchaControl.RenderControl(htmlWriter);
return htmlWriter.InnerWriter.ToString();
}
}
Hope this helps someone who got stuck with the implementation in the book.