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.
Related
I'm developing an ASP.NET MVC app using the latest .NET Framework and VS 2019.
In my page, I have an simple textbox and a search button. When I click the search button and postback the form, in controller, I get model.Code is NULL
Here is my code - my view model:
public class UserManagementViewModel
{
public string Code { get; set; }
// some other properties including simple and complex types
}
In View:
<td style="min-width: 300px">
#Html.TextBoxFor(m => m.Code)
</td>
In Controller:
[HttpPost]
public ActionResult UpdateUserPermissions(UserManagementViewModel model)
{
// model.Code is null here!!
return RedirectToAction("UserManagement");
}
After hours of research, I tried following:
I looked into html source and found
<input id="Code" name="Code">
Using Chrome dev tool, I change the name of the input to "Model.Code"
<input id="Code" name="Model.Code">
And this time, I got the entered value in textbox after postback.
I don't know why this happens and why the element name is not generated correctly!
What can I do?
Edit:
I have some other controls like checkbox and radio-buttons which I use pure HTMl code and name them beginning with "Model." like below instead of using #HTML helper.
And it works fine and I get the value in Action.
Workaround:
I found a workaround here: ASP.NET MVC 4 override emitted html name and id so I change my code to:
#Html.TextBoxFor(m => m.Code, new { #Name = "Model.Code" })
However this code helps me to fix my problem, I still don't know why the name generated by Razor engine is not working.
The helper method #Html.TextBoxFor works absolutely correctly. Basically, you need to use this method for almost all situations (except some situations when you need to use no-standard behavior).
Regarding your case, I think you have trouble with HtmlFieldPrefix.
This property is used for the generation of HTML input's name. We can review MVC source codes in GitHub...
MVC generated HTML input (GitHub link) using this code snippet:
private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes)
{
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (String.IsNullOrEmpty(fullName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
}
TagBuilder tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType));
tagBuilder.MergeAttribute("name", fullName, true);
string valueParameter = htmlHelper.FormatValue(value, format);
bool usedModelState = false;
...
return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing);
}
As we can see name property calculated by method GetFullHtmlFieldName.
This method we can also inspect on GitHub (link):
public string GetFullHtmlFieldName(string partialFieldName)
{
if (partialFieldName != null && partialFieldName.StartsWith("[", StringComparison.Ordinal))
{
// See Codeplex #544 - the partialFieldName might represent an indexer access, in which case combining
// with a 'dot' would be invalid.
return HtmlFieldPrefix + partialFieldName;
}
else
{
// This uses "combine and trim" because either or both of these values might be empty
return (HtmlFieldPrefix + "." + (partialFieldName ?? String.Empty)).Trim('.');
}
}
This method calculates the name using the HtmlFieldPrefix property. So if you change this property you can modify your generated name.
I think you use partial Views and you need to render property with some prefix for correct binding.
Please review next links with examples and possible solutions:
ASP.NET MVC Partial Views with Partial Models
ASP.NET MVC3 add a HtmlFieldPrefix when calling Controller.PartialView
Define a custom HTML prefix for your .NET MVC models
in other to sumbmit model back you have to use form tags. MVC sometimes counts name as model property, but it is better to do it explicitly. This code was tested and both variants of textbox are working properly. And if you use the form you don't need any name at all, other contrary in the most cases you will get null if you use the name and form together.
#model UserManagementViewModel
<form asp-action="UpdateUserPermissions" method="post">
#Html.TextBoxFor(m => m.Code)
// or
<div class="form-group">
<label asp-for="Code" class="control-label"></label>
<input asp-for="Code" class="form-control" />
</div>
<div>
<button type="submit"> submit </button>
</div>
</form>
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.
So what I have is a HTML-Form enabling the user to register for sportsevents. The user can register different profiles (e.g. his children) and every event can potentially have so called "Additional Attributes" like textboxes for T-Shirt-size etc.
#model Models.EventRegistrationModel
#{
Layout = null;
var playerCount = Model.PlayersToRegister.Count;
}
#using (Html.BeginForm("RegisterForEvent", "Event", FormMethod.Post)){
#Html.AntiForgeryToken()
<div class="form-group">
#for (int i = 0; i < playerCount; i++)
{
<div>
<p>#Model.PlayersToRegister[i].User.FullName</p>
</div>
<div
#Html.CheckBoxFor(model => Model.PlayersToRegister[i].PlayerShallGetRegistered)
</div>
//this is the "Additional Attributes"-section for each user-profile
#Html.Raw(Model.PlayersToRegister[i].Form.RenderHtml())
}
</div>
<input type="submit" value="Confirm Registration"/>
}
Since I do not create those events, I cannot know, what these "Additional Attributes" look like, which is why they are rendered dynamically using DynamicForm.
My problem is that I cannot access the user-input for those attributes in the controller. When I check the browser's console, I see the input being posted, but checking the dynamic form's value, it always says "null".
Here's my controller:
[HttpPost, ValidateAntiForgeryToken]
public ActionResult RegisterForEvent(EventRegistrationModel model)
{
for (int i = 0; i < playerList.Count; i++)
{
var form = Session["Form" + i] as Form;
model.PlayersToRegister[i].Form = form;
//var test = form
//var testtest = test.GetResponses(false);
}
return RedirectToAction("EventOverview");
}
As you can see, I tried to use the Form's "GetResponses"-Method, but it returned null.
public List<Response> GetResponses(bool completedOnly)
{
return InputFields.OrderBy(inputField => inputField.DisplayOrder).Select(field => new Response
{
Title = field.Title, Value = field.Response
}).Where(response => !completedOnly || !String.IsNullOrEmpty(response.Value)).ToList();
}
At the moment I am trying to get the values via Session, as this worked in an older version, where you were only able to register one profile at a time. The Session-variable gets assigned in the ActionResult returning the above View.
I've been trying various solutions from various threads over the past days (e.g. ModelState.Clear), but unfortunately nothing has been successful.
If you need more information, code or whatever, please let me know.
Since your form is dynamic you may want to use a dynamic model in the post method. Try something like this:
[HttpPost, ValidateAntiForgeryToken]
public ActionResult RegisterForEvent(FormCollection collection)
{
// stuff....
}
You'll have to do a bit of work parsing out the collection that comes in but it should have everything that was posted from the form. In general I don't recommend this as it can really spiral out of control. Having a well defined view model (as you did in the original posting) is much better in general. However, sometimes you really need something dynamic and this gets the job done.
How can I inspect the post data that are being submited In my application? I want see If there are any data In the input fields when the post are submited.
Html:
<div id="addBox" style="display: none; margin-top: 10px;">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-group">
#Html.Label("Descrption", "Att göra"):
#Html.TextBox("Description", null, new { id = ViewBag.ListContainerID })
<input type="submit" value="Ok" class="btn btn-default" />
</div>
}
</div>
Controller:
[HttpPost]
[ActionName("Details")]
public ActionResult AddTask(FormCollection values)
{
var id = values["id"];
var desc = values["Description"];
/*Task task = new Task
{
Description = desc,
ListContainerID = id
};
db.Tasks.Add(task);
db.SaveChanges();*/
return RedirectToAction("Details");
}
I have tried to set a breakpoint beside the id and desc, but can't find the information about what the input field contains.
In PHP you can use var_dump($_POST) to see information about the POST-data after you hit a submit-button. I want to do the EXACT same thing in ASP.NET
No, you don't want to do that.
Visual Studio has a great debugger that you can use. Set breakpoints and inspect the relevant variables. That way you don't have to change your code when debugging, and you can't forget to remove it afterwards.
You could use the ViewBag to transfer variables from your controller to your view:
// Set in Controller:
ViewBag.TestingVariable1 = values["Foo"];
// Render in View:
#ViewBag.TestingVariable1
Still, this is not recommended. You're changing your application to debug, which is never advisable.
If you insist, look at Response.Write():
Why haven't you added the parameters of the Html.BeginForm(). It should be:
Html.BeginForm("AddTask","ControlerName...",FormMethod.Post)
{}
To check the submit values, you could use ajax and do an async call of the controller method and use alerts/logs to check/print the values (jquery).
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