Imagine a simple form that takes an email input like this:
#using (Html.BeginForm("save", "email", FormMethod.Post, new { #id = "my__form" }))
{
<div class="field">
#Html.LabelFor(model => model.Email)
#Html.ValidationMessageFor(model => model.Email)
#Html.TextBoxFor(model => model.Email, new { #placeholder = "Enter your email", #type = "email" })
</div>
<button type="submit" class="btn">Save email</button>
<div class='spinner'></div>
}
The spinner is not displayed initially (CSS):
.spinner {
display: none;
}
On form submit I show a spinner on the page:
$('.btn').on("click", function (event) {
event.preventDefault();
$('#my__form').submit();
$('.spinner').show();
});
My action is as follows:
[HttpPost]
[Route("email")]
public ActionResult Save(EmailViewModel model)
{
if (ModelState.IsValid)
{
//Do stuff with email
return RedirectToAction("action", "contoller");
}
return View(model);
}
Think of the above as pseudo-code that represents a more generic issue.
If a model state is invalid and the UI is updated in some way (in this case showing a spinner), what's the pattern or mechanism to reset the form?
For clarity, I am not talking here about standard data validation etc. I've tried having a hidden input on the form that I can populate using ModelState.AddModelError("spinner", false) and then acts as a 'switch' that javascript can read and then hide the spinner. This feels like a hack.
It feels like this is a common problem to which there is a common solution?
The hack you mentioned is really how it would be done using normal ASP.NET MVC.
There can be different implementations, such as storing the flag in a ViewBag instead. But the idea is the same.
You might be better off posting the form via AJAX, whose result might include a success flag and/or a list of validation errors. You can then manipulate the DOM in the submit handler via JavaScript based on this result.
Related
I have 2 ASP.NET MVC action methods, I call the first method by passing and load some initial data, then I get some additional details from UI and call the second action method (Post action method from .cshtml). The data I received from the first call is missing in the post method. can anyone help me what am I doing wrong or missing here?
Action methods:
[Route("setprofile")]
public ActionResult SetProfile(string id)
{
ProfileData data = new ProfileData();
//do something
data.name= getData(id);
return this.View(data);
}
[Route("setprofile")]
[HttpPost]
public ActionResult SetProfile(ProfileData data)
{
// Here I'm not missing the data.name field value
}
View .cshtml file:
<div class="panel-body">
#using (Html.BeginForm("SetProfile", "Home", FormMethod.Post))
{
<div>
<h3> Name: #(this.Model.name)</h3>
</div>
<h3>
Comments:#Html.TextBoxFor(m => m.comments)
</h3>
}
I get the comments value but not getting the name field value from the model here.
Note: I need to display the value I received from the first action method as a label, not text box.
There are two things, Name is writen as text and in order to send back to server, you need to put it inside input element.
IF you dont want to show it #Html.HiddenFor(m => m.name) creates hidden input element.
Other than this, check ModelState for validation errors..
if (!ModelState.IsValid)
return BadRequest(ModelState);
.... your code here
if your model is not valid, the ProfileData returns result
You haven't added an input element for it to be sent back to the server when the form is submitted. If you don't want it to be visible, whilst still being posted back, add a hidden field for it:
#Html.HiddenFor(m => m.name)
Without that, all you're doing is rendering name to the markup but, once the form is submitted, it won't be sent back. Alternatively, you could render a textbox for the value whilst setting its readonly attribute. That would allow it to be visible, not changed, and still be sent back to the server.
#Html.TextBoxFor(m => m.name, new { #readonly = "readonly" })
Caution, before You read the rest
This question is not about POST method, redisplaying view with submited form or binding input values to controller method parameters. It's purely about rendering the View using html helper (HiddenFor or Hidden - both returns the same).
I created a simple hidden field using HiddenFor helper
#Html.HiddenFor(m => m.ProductCode)
and my problem is that value for this hidden field is rendered as null:
<input id="productCode" name="productCode" type="hidden" value/>
Even if I set it when instantiating a model and of course it's confirmed with debugging (it always has a value).
So instead it should look like this:
<input id="productCode" name="productCode" type="hidden" value="8888888"/>
Because I know there are some questions like this one (actually all of them refer to changing form values during form POST) I included list of things I tried already. My code is right below this section which I belive to be essential.
So far I tried:
ModelState.Clear() everywhere possible - cause as we know the value from ModelState is first place where it looks for the value. NO EFFECT which I expected, cause my ModelState is empty (my case is not about changing value during POST in controller as in many questions like that), so it should take value from my view model.
Not using HiddenFor helper, but pure html instead. WORKS, but its just workaround, not an answer to the problem.
Duplicating line with helper in view as follows:
#Html.HiddenFor(m => m.ProductCode)
#Html.HiddenFor(m => m.ProductCode)
PARTIALLY WORKS Produces first input as value/> and second as value="8888888"/> which indicates that there is probably something that hides initial property value. Anyway, I found nothing in ViewData at any point, nor in query string. Obviously I can't accept it this way, no explonation needed I guess.
Changing name of the property. Originally it was ProductCode. I changed it to productCode, ProdCode, ProductCodeasd, etc. and all of these WORKS. Again, it looks like there is something that hides/updates the value, but again - 100% sure there is no JS or anything else doing it. So still no answer found - just workaround again.
Explicitly setting the value for HiddenFor: #Html.HiddenFor(x => x.ProductCode, new {Value = #Model.ProductCode}). NO EFFECT, renders the same way.
Using #Html.Hidden instead of #Html.HiddenFor. NO EFFECT, with name set as ProductCode it renders the same way.
One more thing I found interesting. Reading html with Display page source in Chrome [ctrl+U] shows that value is valid value="8888888"/>, but in DevTools it's still value/> and of course submitting the form passes null to Controller method.
Model
public class Product
{
public string Description { get; set; }
public int Quantity { get; set; }
public string ProductCode { get; set; }
public string ImageUrl { get; set; }
public Product(string desc, string productCode, string imgUrl)
{
Description = desc;
ProductCode = productCode;
ImageUrl = imgUrl;
}
}
View
#model Product
#using (Html.BeginForm("UpdateCart", "Cart"))
{
<div class="row pad10">
<div class="col-sm-6 text-center">
<img src="#Model.ImageUrl" width="300" height="300" />
</div>
<div class="col-sm-6 text-justify">
<p>#Model.Description</p>
<div class="row padding-top-2">
<div class="col-sm-6">
<label>#CommonResources.Quantity: </label>
</div>
<div class="col-sm-4">
#Html.TextBoxFor(m => m.Quantity, new
{
#class = "form-control",
#data_val_required = CommonResources.FieldRequired,
#data_val_number = CommonResources.ValidationNumber
})
#Html.ValidationMessageFor(model => model.Quantity, "", new { #class = "text-danger" })
#Html.HiddenFor(m => m.ProductCode)
</div>
</div>
</div>
</div>
<div class="text-center col-xs-12 padTop20 padBottom20">
<input type="submit" value="Submit" class="whtBtn pad" />
</div>
}
Controller
The view is returned from controller with RedirectToAction as follows:
ValidateAndProceed -> ResolveNextStep (here redirection occurs) -> ShowProduct
public ActionResult ValidateAndProceed()
{
var order = Session.Current.Order;
var lang = LangService.GetSelectedLanguage();
var invoice = Session.Current.CurrentInvoice;
var localCurrency = Session.Current.LocalCurrencyInfo;
List<CheckoutValidationFieldError> errors = new List<CheckoutValidationFieldError>();
errors = ValidationService.ValidateAddress(order);
if (errors.Count > 0)
{
return RedirectToAction("InvalidAddress", "Address", new { serializedErrors = JsonConvert.SerializeObject(errors) });
}
return ResolveNextStep(order, invoice);
}
public ActionResult ResolveNextStep(IOrder order, IInvoice invoice)
{
if (OrderService.ShowProductView(order, invoice))
{
return RedirectToAction("ShowProduct");
}
return RedirectToAction("Summary");
}
public ActionResult ShowProduct()
{
Product model = ProductService.GetProduct(Session.Current.CurrentInvoice);
return View("~/Views/Product.cshtml", model );
}
Finally, what can cause such a weird behavior? I've already ran out of options. Maybe anyone had problem like mine before, would appreciate any clue on this case.
I debugged the whole process of rendering the view (got into .Net sources) checking every possible place that could make it fail and found nothing.
After #AndyMudrak and #Jeremy Lakeman comments I decided to try again to find JavaScript responsible for that behavior, but deeper than I did before. What I found was a really silly script where element Id is being concatenated from three strings what I didn't expect, cause it's really badly implemented. So finally - JavaScript is doing it and there is no bad behavior from framework etc.
Actually I am a bit disappointed (even if it's good to know this easy answer) cause it looked much more complicated than it really was and it took me hours to find out how simple it is :|
Thanks for comments, sorry for final simplicity.
I have a view as abcd.cshtml with below code
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.id)
#Html.HiddenFor(model => model.createtime)
<h3>Headline</h3>
<div class="editor-field">
#Html.EditorFor(model => model.general)
#Html.ValidationMessageFor(model => model.general)
</div>
<a class="anchor" id="keywords"></a>
<h3>Keywords</h3>
<div class="editor-field">
#Html.EditorFor(model => model.keywords)
#Html.ValidationMessageFor(model => model.keywords)
</div>
<a class="anchor" id="relatedcq"></a>
<h3>Related CQ</h3>
<div class="editor-field">
#Html.EditorFor(model => model.relatedcq)
#Html.ValidationMessageFor(model => model.relatedcq)
</div>
<p>
<input type="submit" value="Create" class="btn btn-primary" />
</p>
}
</div>
The controller is simple abcd.cs, I just put this into a DB
[HttpPost]
[ValidateInput(false)]
public ActionResult Create(staging staging)
{
staging.modifiedby = User.Identity.Name;
staging.lastmodified = DateTime.Now;
staging.createtime = DateTime.Now;
try
{
db.stagings.Add(staging);
db.SaveChanges();
}
catch (InvalidOperationException e)
{
ViewData["error"] = "An error has occured: " + e.Message;
return View(staging);
}
return RedirectToAction("Details/" + staging.id);
}
What I want is to make sure that Keywords is filled. If Keywords is filled I need to have a pop up window saying "Please fill the Keywords".
I tried doing that using MessageBox.Show() but then for that I had to add System.Windows.Forms and that had some conflicts with System.Web.Mvc;
If you're using htmlhelper ValidationMessageFor would'nt you rather display a Validation Summary to the user the shows him/her all the fields that you require filled in?
e.g.
If you have a model and you have multiple fields that need to be filed in, which can be validated by decorating those fields/properties with the [Required] attribute, or any other that you see fit e.g. [StringLength] etc.
If you do that you can provide a validation summary, using the model binder to not post youre data if it doesnt meet the set validation.
Validation summary example:
#Html.ValidationSummary(false, "Please provide the details above and then click submit.")
That above will display all the validation errors of all fields marked with the following e.g. #Html.ValidationMessageFor(model => model.relatedcq)
Image of how the output, if there are validation errors, will be displayed.
Hope this helps :)
To achieve this you need to create a custom validation attribute on server side. So for e.g. Let's name it as MustBeFilled. Your new attribute would look like:
public class MustBeFilledAttribute : ValidationAttribute, IClientValidatable // IClientValidatable for client side Validation
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
{
return new ValidationResult(FormatErrorMessage(null));
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "mustbefilled",
ErrorMessage = ErrorMessage //Added
};
modelClientValidationRule.ValidationParameters.Add("param", metadata.DisplayName); //Added
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
}
Now you would need a client side script as well to take action if the rule fails. As in your case you want to show a popup. Add below to your site script and make sure it renders after the Jquery-validation-* scripts are rendered.
(function ($) {
$.validator.addMethod('mustbefilled', function (value, element, params) {
if ($(element).val() != '')
return true;
alert('Fill it first.');
// Here you need to Invoke the modal popup.
return false;
}, '');
$.validator.unobtrusive.adapters.add('mustbefilled', ['param'], function (options) {
options.rules["mustbefilled"] = '#' + options.params.requiredif;
options.messages['mustbefilled'] = options.message;
});
})(jQuery);
To apply this custom validation, first add the attribute on your Model property.
[MustBeFilled]
public string Keywords { get; set; }
Then add the custom javascript to bundle.config, only if you have kept the code in separate file named mustbefilled.js. Here I intentionally added the javascript file with validation plugin so you don't get exception if it rendered before the validation plugin.
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate*",
"~/Scripts/mustbefilled.js"));
That's it you're all set.
Now all you have to do is create a model pop and call it in the mustbefilled.js where I placed the comment to invoke it. See this sample here which would help you create a bound modal popup to your keywords property.
I have read many posts on this subject matter, but I am still battling with my issue. I have a form that has a 14 properties and 5 complex types with custom template editors. Client side validation works for all properties and custom template editors, but the property below. If I enter text, it erases the entire contents of the text box with no message. However if I enter 2., I receive the error message saying it must be an number. Has anyone experienced this behavior? This happens on a few of my other forms as well.
As a side note: Should client side validate the nullable int range? Seems like it does not unless I add a range validator. Thank you.
UPDATE
This is actually happening for all my int? properties
Model
[Display(Name = "Lifetime")]
public int? Life { get; set; }
View
#using (Html.BeginForm("EditPerson", "Maintenance", FormMethod.Post, new { #class = "operation-form" }))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.PersonId)
#Html.Hidden("ModifiedBy", User.Identity.Name)
<div class="form-horizontal">
#Html.ValidationSummary(false, "", new { #class = "text-danger" })
<div class="col-md-4">
<div class="row person-field">
<div class="col-md-6 field-name">
#Html.LabelFor(model => model.Life)
</div>
<div class="col-md-6">
#Html.EditorFor(model => model.Life, new { htmlAttributes = new { #class = "to-center routebox" } })
#Html.ValidationMessageFor(model => model.Life, "", new { #class = "text-danger" })
</div>
</div>
</div>
</div>
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
If your using a modern browser, the problem may be the use of #Html.EditorFor() By default this will add type="number" to the input and as a result will generate the browsers implementation of a number control. I have only tested this using Chrome, but it appears that if you enter text that not a valid number, the the browser intercepts it and prevents the value attribute being set. Now when you post back, Life is null and therefore valid and when you return the view, the ModelState value is null explaining why it appears to "erase the entire contents of the text box with no message".
You can test this by changing it to #Html.TextBoxFor(m => m.Life) which will render type="text". Now when you enter text and try to submit, unobtrusive validation will prevent submission and display "The field Life must be a number". As for validating the range, if you do not add an appropriate attribute to the property (e.g [Range(0, int.MaxValue)]) then there is nothing for jquery.validate.unobtrusive to test so it posts back, however its validated on the server and when you return the view, a validation error will be displayed such as "The value '999999999999999999999999999999999999999999' is invalid".
Once I figured out that int? was the problem, I found a solution here:
Nullable int validation not showing message for non-numeric values
All I did was add the data type annotation to all int?. Not sure why this works yet.
[Display(Name = "Lifetime")]
[DataType(DataType.Text)]
public int? Life { get; set; }
There are such options:
Use [Range(0, int.MaxValue)]
Use [RegularExpression("([0-9]+)")]
Use [Integer] from DataAnnotationExtensions
Use your own custom ValidationAttribute with registering on client-side
I have an exercise edit form with various input fields. This form posts to a Save controller action after the user fills out the exercise details and finally clicks the save submit button. Within this form is a link (right now an ActionLink) that redirects the user to another page to choose an exercise from a catalog (this is helpful so the user can choose the exercise from a nice catalog with images instead of just a drop down of exercise names on the exercise edit form). After the user chooses an exercise from the catalog then they are redirected back to the exercise edit page, where I pass the exerciseId that was chosen from the catalog page. The exercise name drop down is then set to the exercise that was chosen from the catalog page. The user can then save the exercise details.
The problem I'm having is that I can't figure out a good way to persist the user's input field values after the redirect to the exercise catalog page. When the user finally gets redirected back to the exercise edit page then the exercise details that the user had filled out prior to the redirect are gone (which makes sense, because I'm not correctly persisting them currently.
My current method for trying to persist the user's input values is to use an actionlink to send the exercise model values as anonymous object parameters to the redirect controller action, however the model property values are null or 0 when passed. It seems like the actionlink doesn't pass the input values that the user entered.
I thought I could use jQuery to take care of the redirect stuff, so I tried doing a jQuery .post, however I couldn't immediately redirect to another controller action once on the server, I'd have to go into the .post callback function and then set window url to the redirect controller action. However, this seems inefficient with the multiple trips to the server.
I feel like a solution would be to just pass these input values in a form submission, however I'd then have a form within a form, which I tried and couldn't get to work.
#using (Html.BeginForm("Edit", "WorkoutPlanExercise",
FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="editExercise">
#Html.ValidationSummary(true)
#Html.HiddenFor(m => Model.WorkoutPlanExerciseId)
#Html.HiddenFor(m => Model.WorkoutPlanId)
#Html.HiddenFor(m => Model.ImageMimeType)
<div class="editRow">
<div class="editExerciselbl">
#Html.LabelFor(m => Model.Name):
</div>
<div class="editExerciseInput">
#Html.DropDownListFor(
x => x.SelectedExerciseId,
Model.AllExercises,
null,
new { #id = "ddlExercise" }
)
</div>
<span>
#Html.ActionLink("Browser Exercise Catalog", "ChooseCatalogExercise",
new {
workoutPlanExerciseId = Model.WorkoutPlanExerciseId,
workoutPlanId = Model.WorkoutPlanId,
reps = Model.Reps,
sets = Model.Sets,
weight = Model.Weight
})
</span>
</div>
<div class="editRow">
<div class="editExerciselbl">
#Html.LabelFor(m => Model.Reps):
</div>
<div class="editExerciseInput">
#Html.EditorFor(m => Model.Reps)
#Html.ValidationMessageFor(m => m.Reps)
</div>
</div>
<div class="editRow">
<div class="editExerciselbl">
#Html.LabelFor(m => Model.Weight):
</div>
<div class="editExerciseInput">
#Html.EditorFor(m => Model.Weight)
#Html.ValidationMessageFor(m => m.Weight)
</div>
</div>
<div class="editRow">
<div class="editExerciselbl">
#Html.LabelFor(m => Model.Sets):
</div>
<div class="editExerciseInput">
#Html.EditorFor(m => Model.Sets, new { #Value = "0" })
#Html.ValidationMessageFor(m => m.Sets)
</div>
</div>
<div id="exerciseImageContainer">
#Html.Partial("Edit.ExerciseImage", Model)
</div>
<input id="save" type="submit" value="Save" />
#Html.ActionLink("Cancel and return to List", "Index", new { workoutPlanId = Model.WorkoutPlanId })
</div>
}
controller:
public ActionResult ChooseCatalogExercise(int workoutPlanExerciseId, int workoutPlanId, int reps, int sets, decimal weight)
{
return RedirectToAction("ChooseCatalogExercise", "ExerciseCatalog",
new { workoutPlanExerciseId = model.WorkoutPlanExerciseId, workoutPlanId = model.WorkoutPlanId, reps = model.Reps, sets = model.Sets, weight = model.Weight });
}
Some possibilities I can think of:
Use session state on the server to maintain and repopulate the values when rendering the page back out when returning to the catalog page. (Do not do this if you have to scale)
Use a cookie to hold the state of the fields and repopulate them when returning (probably not going to work as you will hit the cookie length limit)
Instead of redirecting to another page, use a jquery ui dialog, bootstrap modal, or some other pop-over to select the exercise
I think the 3rd is the best. It makes your site more responsive anyway.
"It seems like the actionlink doesn't pass the input values that the
user entered."
You need a form submit in order to pass the values to server side. Therefore, action link doesn't submit the form values.
In your scenario I think you may need to change the view structure. Something like, avoiding navigating to another view in between. You may use a Jquery solution similar to following.
http://www.ericmmartin.com/projects/simplemodal-demos/