Why do #Html.HiddenFor set value for other model property than specified? - asp.net-mvc-3

I'm working on a ASP.NET MVC3 project where I've defined the following model:
public class Model{
[Key]
int Id { get; set; }
int MappedId { get; set; }
}
This model is passed from a Controller to a Razor view, as follows:
public class Controller:Controller{
public ActionResult Edit(int id) {
Model model = repository.Get(id);
return View(model);
}
}
The model properties are rendered in the view:
#using(Html.BeginForm()) {
#Html.HiddenFor(model => model.Id);
#Html.HiddenFor(model => model.MappedId)
}
What's strange is that the both the hidden inputs get the same value, even though the model properties have different values when passed to the View. E.g. if the Model.Id has value 0 and Model.MappedChannelId have value 7, both hidden inputs are set to the same value 7:
<input id="Id" class="valid" type="hidden" value="7" name="Id" data-val-required="The Id field is required." data-val-number="The field Id must be a number." data-val="true">
<input id="MappedId" class="valid" type="hidden" value="7" name="MappedId" data-val-required="The MappedId field is required." data-val-number="The field MappedId must be a number." data-val="true">
Does anyone have an idea about why Html.HiddenFor(model=> model.Id) set the value to the same value as model.MappedId, even if model.Id has a different value when passed to the View?

My guess is you get the MappedId value from the id parameter of your action. Then you set the Id property with some other value. However in your view you still have Id value from the action parameter and default model binder uses that value for binding Id property. It basically ignores the value from the model instance.
You can solve it by changing the name of Id property in your model. If you don't want that, you should change the name of the action parameter. However you need to create a custom route for it if you don't want to pass its value by querystring.

Related

ValidationMessage strange behaviour with custom attribute

I have a problem with displaying validation message in a Blazor app.
I have the following model (for testing purposes):
public class MyModel
{
[Required(ErrorMessage = "Amount is required")]
public int Amount { get; set; }
[Required(ErrorMessage = "NullableAmount is required")]
public int? NullableAmount { get; set; }
[RequiredIf(nameof(Foo), new[] { "bar" }, ErrorMessage = "Id is required")]
public int Id { get; set; }
[RequiredIf(nameof(Foo), new[] { "bar" }, ErrorMessage = "NullableId is required")]
public int? NullableId { get; set; }
public string Foo { get; set; }
}
I decorated the properties with the built-in RequiredAttribute, and created a custom RequiredIfAttribute, here's the code for that:
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _otherPropertyName;
private readonly string[] _otherPropertyValues;
public RequiredIfAttribute(string otherPropertyName, string[] otherPropertyValues)
{
_otherPropertyName = otherPropertyName;
_otherPropertyValues = otherPropertyValues;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
var type = instance.GetType();
var propertyValue = type.GetProperty(_otherPropertyName)?.GetValue(instance);
if (!_otherPropertyValues.Contains(propertyValue?.ToString()))
{
return ValidationResult.Success;
}
if(value == null)
{
return new ValidationResult(ErrorMessage);
}
if(int.TryParse(value.ToString(), out var intValue))
{
if(intValue == 0)
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
}
I takes the name of the other property, the values for that property, and if the value of that other property matches one of the specified values, it checks if the property decorated with RequiredIf is null, or if it is an integer, 0. (I use it for a model with a nullable int property, which should not be null or 0 if the other property has a certain value).
On the WebAPI side it works fine, but in the Blazor form I have some problems. Here's my form:
<EditForm Model="MyModel" OnValidSubmit="OnSubmit">
<div style="display: flex; flex-direction: column; width: 300px;">
<DataAnnotationsValidator />
<ValidationSummary />
<label>
Foo:
<InputText #bind-Value="MyModel.Foo" />
</label>
<ValidationMessage For="#(() => MyModel.Foo)" />
<label>
Amount:
<InputNumber #bind-Value="MyModel.Amount" />
</label>
<ValidationMessage For="#(() => MyModel.Amount)" />
<label>
Null Amount:
<InputNumber #bind-Value="MyModel.NullableAmount" />
</label>
<ValidationMessage For="#(() => MyModel.NullableAmount)" />
<label>
Id:
<InputNumber #bind-Value="MyModel.Id" />
</label>
<ValidationMessage For="#(() => MyModel.Id)" />
<label>
Null Id:
<InputNumber #bind-Value="MyModel.NullableId" />
</label>
<ValidationMessage For="#(() => MyModel.NullableId)" />
<button type="submit">submit</button>
</div>
</EditForm>
I open the form, enter the value "bar" for Foo, than click submit, I get the following result:
Amount is valid, as it has the default value 0, and RequiredAttribute only checks for nulls.
NullAmount is invalid, it is shown in the ValidationSummary, the input field is also red, as it is invalid, and the validation message is also shown.
Id is invalid, as in the RequiredIf attribute I check int value against 0, it has the default 0 value, the error message is shown in the ValidationSummary, and here comes the interesting part, the input field is not red, and the validation message is not shown.
The same applies for the Nullable Id field, the error message is shown in the validation summary, but the field is not red, and the validation message is not shown.
Now, if I enter a valid value for Id and NullableId (the fields with my custom attribute), and after that I change Id to 0, and NullableId to empty, this is what happens:
Both input fields change to red, validation message is shown for both properties, but in the validation summary, both error messages are displayed twice.
Now, if I click submit, both input fields change to green, and the validation messages are gone, but still displayed in the summary.
I'm totally lost, don't know if I have a problem with my custom validation attribute, a problem in the Blazor component, or it is an unknown issue. The built-in Required attribute works fine, I just added it to the code to see if my form works with that.
Do you have any idea?
I've finally found the problem. In the validation attribute, when returning an error result, I have to pass memberName in the constructor.
return new ValidationResult(ErrorMessage, validationContext.MemberName)
Thank you so much #petyusa, I also ran into this issue and it was driving me nuts. I stumbled across your solution and it solved it immediately. However, I had to pass an array of string to the second argument (perhaps because I'm using .NET 5.0) instead of a simple string:
return new ValidationResult(ErrorMessage, new []{validationContext.MemberName});

Dynamically created elements are not validated on the client

I have a table in my web application, which is populated from the model, where properties have attribute for validation:
[Required(ErrorMessage = "Please enter amount!")]
[DisplayFormat(NullDisplayText = "", ApplyFormatInEditMode = true)]
public decimal? Amount { get; set; }
When I press Submit, the field is properly validated on the client, displaying an error message, if the amount is left empty.
Now the user can add new TRs to the table using jquery. The new record completely imitates the existing records, e.g. when I inspect the Amount field for an existing TR:
<input class="form-control" type="text" data-val="true" data-val-number="The field Amount must be a number." data-val-required="Please enter amount!" id="Financials_1__Amount" name="Financials[1].Amount" value="1834.09"><span class="text-danger field-validation-valid" data-valmsg-for="Financials[1].Amount" data-valmsg-replace="true"></span>
Cf. to the same field of the dynamically added TR:
<input class="form-control" type="text" data-val="true" data-val-number="The amount must be a number." data-val-required="Please enter amount!" id="Financials_77e9f261-010a-4c7c-ae50-e3f6587a8c4e__Amount" name="Financials[77e9f261-010a-4c7c-ae50-e3f6587a8c4e].Amount" value="33"><span class="text-danger field-validation-valid" data-valmsg-for="Financials[77e9f261-010a-4c7c-ae50-e3f6587a8c4e].Amount" data-valmsg-replace="true"></span>
The records look very similar. Yet when I leave the required field empty and press submit, the value is not validated on the client, and the execution comes to the controller's action method. There ModelState.IsValid is false though. This is my first issue.
Here is the action method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int contractorId, ContractorDetailsDto dto)
{
if (ModelState.IsValid)
{
...
}
return View(_contractorRepository.GetContractorDetailsViewModelByDto(dto));
}
As our ModelState is invalid, the same view is loaded. There the error is displayed in the validation summary, BUT NOT UNDER THE FIELD WITH THE INVALID VALUE. This is the second issue.
I have on my view:
#section Scripts {
<partial name="_ValidationScriptsPartial" />
}
How can I find the cause of my problems?
Answering for your main question. Can you try to execute next javascript code after you add new row
function refreshValidators(formSelector) {
var targetForm = $(formSelector);
targetForm.removeData('validator');
targetForm.removeData('unobtrusiveValidation');
targetForm.removeAttr('novalidate');
$.validator.unobtrusive.parse(targetForm);
}
Regarding your second problem I think you should create separate question for it as it unrelated to your main question. You need to find proper way how to add new rows in ASP.NET Core. For ASP.NET MVC we had next solution how to solve this task http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/ . After googling BeginCollectionItem I found core analog solution, but it is pretty old and I didnt use it personaly.

How to bind a list of objects in ASP.NET Core MVC when the posted list is smaller than original?

Using "disabled" attribute on inputs on form does not post them, which is expected and wanted. However, if you prepare a form of 3 objects in a list, disable the first and third, and submit, the 2nd object appears in post header, but does not bind to the list correctly, because it has an index [1] instead of [0].
I understand how model binding works and why it does not bind the posted object that I want, but I don't know how else to describe the problem to get specific results that would lead me to my solution. Anything I search for leads to basic post and binding examples.
List inside the model I'm using:
public IList<_Result> Results { get; set; }
Class _Result has one of the properties:
public string Value { get; set; }
I fill up the list and use it in view like so:
#for (int i = 0; i < Model.Results.Count; i++)
{
...
<td>
<input asp-for="Results[i].Value" disabled />
</td>
...
}
I have checkboxes on form, which remove (with javascript) the "disabled" attribute from the inputs and thus allow them to be posted.
When I fill up the said list with 3 _Result objects, they are shown on form and all have the "disabled" attribute. If I remove the "disabled" attribute from the first two objects and click on submit button, I receive the Results list with first 2 _Result objects, which is as expected.
However, if I remove the "disabled" attribute only from the second _Result object (the first _Result object still has "disabled" attribute), the Results list comes back empty in my Controller method.
In my Form Data Header, I see this: "Results[1].Value: Value that I want posted", which means that post occurs, but list does not bind the object due to the index.
Any idea on how I can achieve that proper binding? Also, the reason I'm using "disabled" attribute is because I'm showing many results on a single page and want to only post those that are selected.
For getting selected items, you could try checkbox with View Model instead of using jquery to control the disable property.
Change ViewModel
public class ModelBindVM
{
public IList<_ResultVM> Results { get; set; }
}
public class _ResultVM
{
public bool IsSelected { get; set; }
public string Value { get; set; }
}
Controller
[HttpGet]
public IActionResult ModelBindTest()
{
ModelBindVM model = new ModelBindVM
{
Results = new List<_ResultVM>() {
new _ResultVM{ Value = "T1" },
new _ResultVM{ Value = "T2" },
new _ResultVM{ Value = "T3" }
}
};
return View(model);
}
[HttpPost]
public IActionResult ModelBindTest(ModelBindVM modelBind)
{
return View();
}
View
<div class="row">
<div class="col-md-4">
<form asp-action="ModelBindTest">
#for (int i = 0; i < Model.Results.Count; i++)
{
<input type="checkbox" asp-for="Results[i].IsSelected" />
<label asp-for="#Model.Results[i].IsSelected">#Model.Results[i].Value</label>
<input type="hidden" asp-for="#Model.Results[i].Value" />
}
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

Save and retrieve checkbox values asp.net mvc3

I am a newbie to MVC3 technology and trying to workout my way get through a small problem.
I simply need to get checked checkbox values to be saved in database and on Edit view check them back.
<input type="checkbox" value="Photo" name="DocSub" /> Photograph<br />
<input type="checkbox" value="BirthCertificate" name="DocSub" /> Copy Of Birth Certificate<br />
<input type="checkbox" value="School Leaving Certificate" name="DocSub" /> School Leaving Certificate<br />
When the Submit button is clicked, the [HTTPPOST] Action method of the desired controller is called. There I receive the selected values in this form :
var selectedCheckBoxValues = Request.Form["DocSub"];
I am getting the all the checked checkbox values in comma separated form and able to store them to the database, but wondering if this is the right approach to go by.
Also I need to know to retrieve checkbox values from database on Edit view in already checked form.
the typical apporoach to these problems is to use a view with a model
ie, suppose this is view Documents.cshtml
#model DocumentViewModel
#Html.LabelFor(m => m.Photo)
#Html.CheckBoxFor( m => m.Photo )
#Html.LabelFor(m => m.BirthCertificate)
#Html.CheckBoxFor( m => m.BirthCertificate )
#Html.LabelFor(m => m.SchoolLeavingCertificate)
#Html.CheckBoxFor( m => m.SchoolLeavingCertificate )
and use a viewmodel to pass data to the view
the viewmodel is a class where you have the data your going to send to the view, ie.
public class DocumentViewModel{
public bool Photo {get;set;}
public bool BirthCertificate { get; set; }
public bool SchoolLeavingCertificate {get;set;}
}
and you'd have a controller that populates the viewmodel and calls the view
public ActionResult Documents()
{
var modelData = new DocumentViewModel();
//or retrieve from database at this point
// ie. modelData.Photo = some database value
return View(modelData);
}
[HttpPost]
public ActionResult Documents(DocumentViewModel documentsVM)
{
if (ModelState.IsValid)
{
//update the database record, save to database... (do stuff with documentsVM and the database)
return RedirectToAction("NextAction");
}
//else, if model is not valid redirect back to the view
return View(documentsVM);
}
look for tutorials out there on mvc basics. read code.

MVC3 Modelbinder EF4 ICollection property [duplicate]

I'm working on my first ASP.NET MVC 3 application and I've got a View that looks like this:
#model IceCream.ViewModels.Note.NotesViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.TextBoxFor(m => m.Name)
foreach (var item in Model.Notes)
{
#Html.EditorFor(m => item);
}
<input type="submit" value="Submit"/>
}
And I have an EditorTemplate that looks like this:
#model IceCream.ViewModels.Note.NoteViewModel
<div>
#Html.HiddenFor(m => m.NoteID)
#Html.TextBoxFor(m => m.NoteText)
#Html.CheckBoxFor(m => m.IsChecked)
</div>
NotesViewModel looks like so:
public class NotesViewModel
{
public string Name { get; set; }
public IEnumerable<NoteViewModel> Notes { get; set; }
}
NoteViewModel looks like this:
public class NoteViewModel
{
public int NoteID { get; set; }
public System.DateTime Timestamp { get; set; }
public string NoteText { get; set; }
public bool IsChecked { get; set; }
}
The NotesViewModel is populated just fine when it is passed to the view. However when the submit button is clicked, the controller action handling the post has only the value for the Name property of the viewmodel. The Notes property - the list of notes that have been checked/unchecked by the user - is null. I've got a disconnect between the populating of those TextBoxFor and CheckBoxFor elements when the view is displayed and the ViewModel being sent back. Guidance on this?
SOLUTION
Thanks go to Mystere Man for setting me straight on this. As I understand it, essentially by changing my loop to
#Html.EditorFor(m => m.Notes)
changes the underlying HTML, which I understand provides for the proper model binding on the post. Looking at the resulting HTML, I see that I get the following generated for one of the Notes:
<div>
<input id="Notes_0__NoteId" type="hidden" value="1" name="Notes[0].NoteId">
<input id="Notes_0__NoteText" type="text" value="Texture of dessert was good." name="Notes[0].NoteText">
<input id="Notes_0__IsChecked" type="checkbox" value="true" name="Notes[0].IsChecked>
</div>
Which is different than this HTML generated by my original code:
<div>
<input id="item_NoteId" type="hidden" value="1" name="item.NoteId>
<input id="item_NoteText" type="text" value="Texture of dessert was good." name="item.NoteText" >
<input id="item_IsChecked" type="checkbox" value="true" name="item.IsChecked">
</div>
By looping through the Notes, the generated HTML essentially loses any references to the viewmodel's Notes property and while the HTML gets populated correctly, the setting of the checkbox values has no way to communicate their values back to the viewmodel, which I guess is the point of the model binding.
So I learned something, which is good.
You're a smart guy, so look at your view. Then, consider how the HTML gets generated. Then, consider how on postback the Model Binder is supposed to know to re-populate Notes based on the generated HTML.
I think you'll find that your HTML doesn't have enough information in it for the Model Binder to figure it out.
Consider this:
#EditorFor(m => Model.Notes)
Rather than the for loop where you are basically hiding the context from the EditorFor function.
And for those that just want the answer as a for loop:
#for (int x = 0; x < Model.Notes.Count(); x++) {
#Html.HiddenFor(m => m.Notes[x].NoteId)
#Html.EditorFor(m => m.Notes[x].NoteText)
#Html.EditorFor(m => m.Notes[x].IsChecked)
}

Resources