MVC form Post deserialization is incomplete for complex model object - asp.net-mvc-4

Using MVC 4 Forms, I have a model that always contains four children in a List<T> property. The view displays the model correctly with each of the four child models rendered with a Razor partial view. The problem is that when I submit/post, the model deserializes with a null value for the child list.
Model:
public class MyModel
{
public int SomeValue { get; set; }
public List<ChildModel> Children { get; set; }
...
}
View:
#model MyProject.Models.MyModel
#using (Html.BeginForm())
{
#Html.LabelFor(model => model.SomeValue)
#Html.Partial("ChildPartial", Model.Children[0])
#Html.Partial("ChildPartial", Model.Children[1])
#Html.Partial("ChildPartial", Model.Children[2])
#Html.Partial("ChildPartial", Model.Children[3])
<input type="submit" value="Save" />
}
Controller:
public class MyController : Controller
{
public ActionResult Index()
{
MyModel model = new MyModel();
model.Children = new List<ChildModel>();
model.Children.Add(new ChildModel());
model.Children.Add(new ChildModel());
model.Children.Add(new ChildModel());
model.Children.Add(new ChildModel());
return View(model);
}
[HttpPost]
public ActionResult Index(MyModel model)
{
//model.Children is null here
//do stuff
...
return RedirectToAction("Index", "SomeOtherController");
}
}
The ChildPartial views are each rendering correctly, and I am entering values into the controls, but they are not deserialized into the List<ChildModel>. I can only get the root level properties of MyModel to deserialize in the Post method.
I have tried adding UpdateModel(model); to the beginning of the controller Post method but no luck there. Any ideas?
Edit
ChildModel.cs:
public class ChildModel
{
public String Name { get; set; }
public double Ratio { get; set; }
...
}
ChildPartial.cshtml:
#model MyProject.Models.ChildModel
<div>
<div>
<div>
<span>#Model.Name</span>
</div>
<div>
#Html.LabelFor(m => m.Ratio)
#Html.TextBoxFor(m => m.Ratio, new { autocomplete = "off" })
#Html.ValidationMessageFor(m => m.Ratio)
</div>
</div>
...
</div>

I would first recommend you reading about the specific syntax that the default model binder expects and the naming convention when binding to collections: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
Once you compare the names of your input fields with the ones explained in this blog post you will pretty quickly understand why your code doesn't work. You are simply not following the standard naming convention.
In order to fix this I would recommend you using editor templates. So in your main view put the following:
#model MyProject.Models.MyModel
#using (Html.BeginForm())
{
#Html.LabelFor(model => model.SomeValue)
#Html.EditorFor(model => model.Children)
<input type="submit" value="Save" />
}
Then move your ChildPartial.cshtml to ~/Views/Shared/EditorTemplates/ChildModel.cshtml. Notice that the name of the template and the location is extremely important. Make sure you have followed it. And put this inside:
#model MyProject.Models.ChildModel
<div>
<div>
<div>
<span>#Model.Name</span>
</div>
<div>
#Html.LabelFor(m => m.Ratio)
#Html.TextBoxFor(m => m.Ratio, new { autocomplete = "off" })
#Html.ValidationMessageFor(m => m.Ratio)
</div>
</div>
...
</div>
Alright, now run your project, inspect the generated HTML and more specifically the names of the input fields compare them with your initial version and compare them to the blog post I have initially linked to in my answer and you will understand everything about how model binding to collections works in ASP.NET MVC.
Remark: in your child template you don't have a corresponding input field for the Name property of your ChildModel. So don't be surprised if it is null in your controller. You simply never send a value to it when the form is submitted. If you want this to happen you could include it as a hidden field in your editor template:
#Html.HiddenFor(m => m.Name)

Related

IValidationAttributeAdapterProvider is called only for EmailAddressAttribute

What I was doing with ASP.NET MVC 5
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MaxLengthAttribute), typeof(MyMaxLengthAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(MyRequiredAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MinLengthAttribute), typeof(MyMinLengthAttribute));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAddressAttribute), typeof(MyEmailAddressAttributeAdapter));
Now I'm migrating it to ASP.NET core 6
We can't use DataAnnotationsModelValidatorProvider anymore so I'm trying to use IValidationAttributeAdapterProvider, which doesn't work properly for me.
My codes
My IValidationAttributeAdapterProvider is below.
public class MyValidationAttributeAdapterProvider : ValidationAttributeAdapterProvider, IValidationAttributeAdapterProvider
{
IAttributeAdapter? IValidationAttributeAdapterProvider.GetAttributeAdapter(
ValidationAttribute attribute,
IStringLocalizer? stringLocalizer)
{
return attribute switch
{
EmailAddressAttribute => new MyEmailAddressAttributeAdapter((EmailAddressAttribute)attribute, stringLocalizer),
MaxLengthAttribute => new MyMaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer),
MinLengthAttribute => new MyMinLengthAttribute((MinLengthAttribute)attribute, stringLocalizer),
RequiredAttribute => new MyRequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer),
_ => base.GetAttributeAdapter(attribute, stringLocalizer),
};
}
}
My model class is below.
public class LogInRequestDTO
{
[Required]
[EmailAddress]
[MaxLength(FieldLengths.Max.User.Mail)]
[Display(Name = "mail")]
public string? Mail { get; set; }
[Required]
[MinLengthAttribute(FieldLengths.Min.User.Password)]
[DataType(DataType.Password)]
[Display(Name = "password")]
public string? Password { get; set; }
}
And in my Program.cs, I do like below.
builder.Services.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Resources));
});
builder.Services.AddSingleton<IValidationAttributeAdapterProvider, MyValidationAttributeAdapterProvider>();
What happed to me
I expect GetAttributeAdapter is called for each attribute like EmailAddressAttribute, MaxLengthAttribute, etc.
But it's called only once with EmailAddressAttribute.
So, all other validation results are not customized by my adaptors.
If I remove [EmailAddress] from the model class, GetAttributeAdapter is never called.
Am I missing something?
Added on 2022/05/24
What I want to do
I want to customize all the validation error message.
I don't want to customize for one by one at the place I use [EmailAddress] for example.
I need the server side validation only. I don't need the client side validation.
Reproducible project
I created the minimum sample project which can reproduce the problem.
https://github.com/KuniyoshiKamimura/IValidationAttributeAdapterProviderSample
Open the solution with Visual Studio 2022(17.2.1).
Set the breakpoint on MyValidationAttributeAdapterProvider.
Run the project.
Input something to the textbox on the browser and submit it.
The breakpoint hits only once with EmailAddressAttribute attribute.
The browser shows the customized message for email and default message for all other validations.
Below is a work demo, you can refer to it.
In all AttributeAdapter, change your code like below.
public class MyEmailAddressAttributeAdapter : AttributeAdapterBase<EmailAddressAttribute>
{
// This is called as expected.
public MyEmailAddressAttributeAdapter(EmailAddressAttribute attribute, IStringLocalizer? stringLocalizer)
: base(attribute, stringLocalizer)
{
//attribute.ErrorMessageResourceType = typeof(Resources);
//attribute.ErrorMessageResourceName = "ValidationMessageForEmailAddress";
//attribute.ErrorMessage = null;
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-must-be-true", GetErrorMessage(context));
}
// This is called as expected.
// And I can see the message "Input the valid mail address.".
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
In homecontroller:
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index([FromForm][Bind("Test")] SampleDTO dto)
{
return View();
}
Index view:
#model IV2.Models.SampleDTO
#{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<h4>SampleDTO</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Test" class="control-label"></label>
<input asp-for="Test" class="form-control" />
<span asp-validation-for="Test" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Result1:
Result2:
I found the solution.
What I have to use is not ValidationAttributeAdapterProvider but IValidationMetadataProvider.
This article describes the usage in detail.
Note that some attributes including EmailAddressAttribute have to be treated in special way as describe here because they have default non-null ErrorMessage.
I confirmed for EmailAddressAttribute and some other attributes.
Also, there's the related article here.

Model Value is null in mvc4

I am using a post method, where i am trying to post the value of textbox to database, for this i am doing all the necessary steps, but in that post method my model is null. Find the code below,
My Simple Controller
[HttpPost]
public ActionResult Index(QuestionBankModel question)
{
return View();
}
My Model
public class QuestionBankModel
{
public string question { get; set; }
}
My View
#model OnlinePariksha.Models.QuestionBankModel
#{
var CustomerInfo = (OnlinePariksha.Models.UserLoginModel)Session["UserInfo"];
}
#{
ViewBag.Title = "Index";
}
#{
Layout = "~/Views/Shared/Admin.cshtml";
}
#using (Html.BeginForm("Index", "AdminDashboard", FormMethod.Post))
{
<div id="questionsDiv" style="width:100%; display:none;">
<div style="width:200px">
<table style="width:100%">
<tr>
<td><span><b>Question:</b></span></td>
<td>
#Html.TextBox(Model.question, new Dictionary<string, object> { { "class", "textboxUploadField" } })
</td>
</tr>
</table>
</div>
<div class="clear"></div>
<div>
<input type="submit" class="sucessBtn1" value="Save" />
</div>
</div>
}
Did i miss anything?
Your problem is that the POST method parameter name is the same name as your model property (and as a result model binding fails). Change the method signature to
public ActionResult Index(QuestionBankModel model)
{
...
}
or any other parameter name that is not the same as a model property.
By way of explanation, the DefaultModelBinder first initializes a new instance of QuestionBankModel. It then inspects the form (and other) values and sees question="SomeStringYouEntered". It then searches for a property named question (in order to set its value). The first one it finds is your method parameter so it internally it does QuestionBankModel question = "SomeStringYouEntered"; which fails (you cant assign a strung to a complex object) and the model parameter now becomes null.
Html.TextBox is being used incorrectly because the first parameter is the name of the textbox and you're passing the value of question. I would use this instead:
#Html.TextBoxFor(m => m.question)
Have you tried using #HTML.TextBoxFor?
#Html.TextBoxFor(m=>m.question,new Dictionary<string, object> { { "class", "textboxUploadField" } })

creating a partial view with a form post that planning on using in several places

How can I create a partial view with a form post that I plan on using in several places?
The partial view will have a form that creates an entry in data storage and displays the persisted data underneath this form.
So after submitting the form I ll see my entry in a grid like structure under the form without switching the parent view.
If the model is not valid the error will be shown also. The trick here is, how do I stay in my current page without creating an action
In the controller of each view that shows the partial view?
I will be using this partial view in say 10 different parent views.
Below, i provide some of the codes that will help community to make sense the question exactly.
How should i configure my code to achieve my goal.
Thanks
This is the partial view sample
#model ViewModels.CommentViewModel
#using (Html.BeginForm("Index", "Comment", FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary()
<div class="form-group">
<label class="control-label col-md-2">Please Type Your Name</label>
<div class="col-md-10">
#Html.TextBoxFor(model => model.Name, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<input id="addComment" type="submit" value="Add" />
</div>
}
#foreach (var item in Model.Comments)
{
<p>
#item.Name
</p>
}
Here is Controller
public PartialViewResult Index(int id)
{
var model = new CommentViewModel() { Comments= db.Comments.Where(x=> x.NewsId == id && x.isApproved== true )};
return PartialView("_Comments", model);
}
[HttpPost]
public PartialViewResult Comment(int id, CommentViewModel model)
{
if (ModelState.IsValid)
{
var comment = new Comment()
{
Name = model.Name,
Title = model.Title,
CommentContent = model.Content,
Email = model.Email,
CreationDate = DateTime.Now,
RefId = Guid.NewGuid(),
isApproved = false,
NewsId = id
};
db.Comments.Add(comment);
db.SaveChanges();
return PartialView();
}
return PartialView();
}
If you want to do things like submit a form and retrieve updated data without reloading the page, then you're talking about AJAX. The fact that this is a partial view is meaningless in that context. It doesn't matter how many different views this partial view will be rendered in, you just need one action in one controller that can respond to an AJAX request. Then, you'll just need to do something like the following with JavaScript that can be included via an external file in whatever views need this form:
$('#MyPartialViewForm').on('submit', function (e) {
// prevents form from submitting standard way, causing page refresh
e.preventDefault();
$.post('/url/that/handles/form', $(this).serialize(), function (results) {
// results will be a rendered partial with the data here,
// which you can use to replace the content of a div or
// something on your page.
$('#DivWhereSubmittedDataIsDisplayed').html(results);
});
});
Then, in your action that responds to the AJAX request:
[HttpPost]
public ActionResult ActionForAjaxForm(FormModel model)
{
// do something like save the posted model
return PartialView("_PartialViewThatRendersData", model);
}

Implementation of Kendo Listview control in MVC

I was trying to implement Listview control of Kendo UI for MVC. I am trying to bind the list view with my model but I am getting this error :
"CS1977: Cannot use a lambda expression as an argument to a dynamically dispatched operation without first casting it to a delegate or expression tree type"
I have checked some other questions on stackoverflow with the same error but I am unable to know the cause for this error as this is kendo Syntax and there is nothing wrong with my code as far as I know.
The error is in this line::.DataSource(ds => ds
View Page:
#{
ViewBag.Title = "Courses";
}
#using Kendo.Mvc.UI
<h2>Courses</h2>
Back
<div class="bodywrap">
<div class="CommonClass">
#( Html.Kendo().ListView<K_SampleProject.Models.CourseModel>(Model)
.Name("listView")
.TagName("div")
.ClientTemplateId("template")
.DataSource(ds => ds
.Model(model =>
{
//The unique identifier (primary key) of the model is the ProductID property
model.Id(p => p.ProductID);
// Declare a model field and optionally specify its default value (used when a new model instance is created)
model.Field(p => p.ProductName).DefaultValue("N/A");
// Declare a model field and make it readonly
model.Field(p => p.UnitPrice).Editable(false);
})
)
.Pageable()
)
</div>
</div>
<script type="text/x-kendo-tmpl" id="template">
<div class="product">
<img src="#Url.Content("~/content/web/foods/")${ProductID}.jpg" alt="${ProductName} image" />
<h3>${ProductName}</h3>
<dl>
<dt>Price:</dt>
<dd>${kendo.toString(UnitPrice, "c")}</dd>
</dl>
</div>
</script>
Model
namespace K_SampleProject.Models
{
public class CourseModel
{
public List<tbl_Courses> CourseList { get; set; }
public string ProductID { get; set; }
public string ProductName { get; set; }
public string UnitPrice { get; set; }
}
}
Controller
public ActionResult Courses()
{
CourseModel Model = new CourseModel();
RegistrationService ObjService = new RegistrationService();
Model.CourseList = ObjService.GetCourses();
return View(Model);
}
The main error in your code is that you passing single CourseModel class to the list, when it expects the List of CourseModel.
So, your Controller should looks like:
public ActionResult Courses()
{
List<CourseModel> result;
CourseModel Model = new CourseModel();
RegistrationService ObjService = new RegistrationService();
Model.CourseList = ObjService.GetCourses();
result.Add(Model);
return View(result);
}
I also advise:
Add #model List<CourseModel> in top of the View
If it is a PartialView (not main view like index) change return for: return PartialView(result);

ASP.NET MVC 4 - EditorTemplate for nested collections

I have the following model classes (classes simplifies for the purpose of this question):
public class Lesson
{
public Guid Id {get;set;}
public string Name {get;set;}
public List<ExerciseForPupil> Exercises {get;set;}
}
public class ExerciseForPupil
{
public Guid Id {get;set;}
public string Name {get;set;}
public List<ExerciseItemForPupil> ExerciseItems {get;set;}
}
public class ExerciseItemForPupil
{
public Guid Id {get;set;}
public string Content {get;set;}
public string UserValue {get;set;}
}
Now, I want users to be able to fille "UserValue" value for each exercise in the lesson.
Let's say there are 5 exercises for the lesson.
I render these exercises as follows
#Html.EditorFor(x=>x.Lesson.Exercises)
Which renders an EditorTemplate which looks as follows:
#model MyNamespace.ExerciseForPupil
#using (Html.BeginForm("ScoreExercise", "SharedLesson", FormMethod.Post))
{
#Html.Hidden("Id", Model.Id)
#if (Model.ExerciseItems != null)
{
foreach (var exerciseItem in Model.ExerciseItems)
{
#Html.EditorFor(x => exerciseItem, "ExerciseItemForPupil")
}
}
<input type="submit" value="Submit"/>
}
I also have EditorTemplate for "ExerciseItemForPupil":
#model MyNamespace.ExerciseItemForPupil
#Html.HiddenFor(model => model.Id)
#Html.TextBoxFor(model => model.UserValue)
Problem:
As can be seen there will be multiple forms on the page. My "ScoreExercise" action is as follows:
public ActionResult ScoreExercise(ExerciseForPupil exercise)
{
//exercise.ExerciseItems is NULL
}
But my nested collection on the second level (ExerciseItems) is null.
What am I doing wrong?
UPDATE
I've changed the code according to #MysterMan advices:
I call editor template for Exercises as follows:
#Html.EditorFor(x => x.Lesson.Exercises)
and inside this EditorTemplate I call Editor Template for my ExerciseItems
#Html.EditorFor(x=>x.ExerciseItems)
this renders the following markup for UserValue property:
<input id="Lesson_Exercises_0__ExerciseItems_1__UserValue" name="Lesson.Exercises[0].ExerciseItems[1].UserValue" type="text" value="">
but it does not work also
Don't use the foreach. EditorTemplates already iterate over collections if you pass it a collection.
#model MyNamespace.ExerciseForPupil
#using (Html.BeginForm("ScoreExercise", "SharedLesson"))
{
#Html.HiddenFor(x => x.Id)
#Html.EditorFor(x => x.ExerciseItemsForPupil)
<input type="submit" value="Submit"/>
}
A few things to note. You don't have to pass the template name, as the template name is already the same name as the item type. You don't have to use the Post formmethod, as that's the default. There is no name of the property for List so I just assumed it was the plural.
Your last class is also illegal, you would not specify it as a List like that.