ViewModel data is all null when passed to my controller? [duplicate] - c#

This question already has answers here:
Asp.Net MVC: Why is my view passing NULL models back to my controller?
(2 answers)
Closed 6 years ago.
I'm trying to follow best practices to add data validation to my UI. I want to add the data validation to the ViewModel and then if that data is valid, submit the form to the controller. However, the data the controller receives is always null values. Any idea what I'm doing wrong? This whole MVVC architecture is confusing me. I had it working when I was submitting the form using the model but I can't get data validation on the model?
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> CreateResource(AddResourceViewModel model)
{
if (ModelState.IsValid)
{
await (//does something);
}
return View();
}
ModelView:
public class AddResourceViewModel
{
public string User { get; set; }
public string ResourceName { get; set; }
public int ResourceId { get; set; }
public string Model { get; set; }
public float Latitude { get; set; }
public float Longitude { get; set; }
public decimal Amount { get; set; }
public int UnitId { get; set; }
public int PrimaryEsfId { get; set; }
public IEnumerable<string> Capabilities { get; set; }
public IEnumerable<int> AdditionalEsfs { get; set; }
public Resource Resource { get; set; }
}
Beginning of cshtml form:
#model erms.ViewModel.AddResourceViewModel
<form asp-controller="AddResource" asp-action="NewResource" method="post" class="form-horizontal">
<div class="panel panel-default">
<div class="panel-body">
<form asp-controller="AddResource" asp-action="CreateResource" method="post" class="form-horizontal">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ResourceId" class="col-md-2 control-label">Resource ID:</label>
<div class="col-md-10">
<input asp-for="ResourceId" class="form-control" value="#Model.Resource.ResourceId" readonly />
<span asp-validation-for="ResourceId" class="text-danger"></span>
</div>
</div>

replace return View(); to return View(model);.

This kind of stuff makes me so frustrated when learning new architecture but I've figured it out. It is a naming convention issue. I've addressed the issue and it is now working properly:
The conflicting names were:
public string Model { get; set; }
from my ViewModel, and:
public async Task<IActionResult> NewResource(AddResourceViewModel model)
from my controller. So the Model is conflicting with the model...
According to: http://ideasof.andersaberg.com/development/aspnet-mvc-4-model-binding-null-on-post
Do not name your incoming variables in the Action the same as you do in the model being posted. That will mess up the Model Binder.

Perhaps I'm thinking the problem would be you're not using razor to submit the form. This is what I have in mind:
#model <AddResourceViewModel>
#using(Html.BeginForm()) {
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ResourceId" class="col-md-2 control-label">Resource ID:</label>
<div class="col-md-10">
#Html.TextboxFor(x => x.ResourceId)
#Html.ValidationMessageFor(x => x.ResourceId)
</div>
</div>
}
Since this is what I usually use to validate my form, perhaps this is worth considering. I might be wrong though.

Related

ASP .NET Core 5 Razor Pages: how to properly use Partial View and validate it's model state?

I'm using ASP .NET Core 5 Razor Pages.
My goal is to has a set of Partial Views (for reusability purpose) that I can use on several pages. Each Partial View has a form with its own custom post event handler (it will be processed by a code-behind of a pages that will contain this Partial View).
N.B. Some pages can contain two or even more different Partial Views! And I need that Partial View models to be validated independently of each other (in two separate custom event handlers).
Here is simplified code that I use for today. Partial View model (contains some data for a user):
public partial class User
{
[Required]
public string Name { get; set; }
[Required]
public string Surname { get; set; }
}
public class UserModel : PageModel
{
[BindProperty]
public User user { get; set; }
[TempData]
public string StatusMessage { get; set; }
public UserModel()
{
user = new User();
}
}
_UserPartial.cshtml (displays that user data):
#model UserModel
<div class="row text-warning">
<div class="col-md-4">
<form method="post" asp-page-handler="UserEdited">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="user.Surname" class="control-label"></label>
<input asp-for="user.Surname" class="form-control" />
<span asp-validation-for="user.Surname" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="user.Name" class="control-label"></label>
<input asp-for="user.Name" class="form-control" />
<span asp-validation-for="user.Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save user data" />
</div>
</form>
</div>
</div>
Index.cshtml (main page that contains Partial View):
#page
#model IndexModel
#{
ViewData["Title"] = "Main page";
}
#if (!String.IsNullOrWhiteSpace(#Model.StatusMessage))
{
<div class="text-center">
<h4 class="text-warning">#Model.StatusMessage</h4>
</div>
}
<div class="text-center" id="mainView">
<p>Some text in a main view</p>
<p>Some link in a main view.</p>
</div>
<div class="text-center" id="userPartialView">
#{await Html.RenderPartialAsync("_UserPartial", IndexModel.userModel);}
</div>
//Some other Partial View (which contains some data for a message)
<div class="text-center" id="userPartialView">
#{await Html.RenderPartialAsync("_MessagePartial", IndexModel.messageModel);}
</div>
Index.cshtml.cs (code-behind of a main page):
public class IndexModel : PageModel
{
public static UserModel userModel { get; set; }
//A model for some other Partial View (which contains some data for a message)
public static MessageModel messageModel { get; set; }
[TempData]
public string StatusMessage { get; set; }
public IActionResult OnGet()
{
userModel = new UserModel();
messageModel = new MessageModel();
return Page();
}
public IActionResult OnPostUserEdited()
{
if (!userModel.ModelState.IsValid)
{
return Page();
}
StatusMessage = "User data was saved!";
return RedirectToPage();
}
}
Problem is that userModel.ModelState is always valid even if Name and Surname are empty:
Looks like UserModel is not validaiting at all.
And I have a strong feeling that I'm using Partial Views tha wrong way (not the way they were supposed to be used).
So what's wrong with my code? How to properly use Partial View and validate it's model state? Any help is appreciated.
You don't need to have a Page Model for the partial view. Just add it as a Razor View.
Index.cshtml.cs
[BindProperty]
public User userModel { get; set; }
[BindProperty]
public Message messageModel { get; set; }
[TempData]
public string StatusMessage { get; set; }
public void OnGet()
{
userModel = new User();
}
public IActionResult OnPostUserEdited()
{
ModelState.Clear();
if (!TryValidateModel(userModel))
{
return Page();
}
StatusMessage = "User data was saved!";
return RedirectToPage();
}
public IActionResult OnPostMessageEdited()
{
ModelState.Clear();
if (!TryValidateModel(messageModel))
{
return Page();
}
StatusMessage = "Message data was saved!";
return RedirectToPage();
}
Index.cshtml:
<div class="text-center" id="userPartialView">
#{await Html.RenderPartialAsync("_UserPartial", Model.userModel);}
</div>
<div class="text-center" id="messagePartialView">
#{await Html.RenderPartialAsync("_MessagePartial", Model.messageModel);}
</div>

ASP.NET Core 5.0 - POST on custom model

I did it according to youtube tutorial, but unfortunately for me it behaves extremely oddly.
Simple scenario: add comment to a post.
public class CommentViewModel
{
public Post Post { get; set; }
public Comment Comment { get; set; }
}
<p>#Model.Post.Title</p>
<p>#Model.Post.Body</p>
<form method="post" asp-action="NewComment">
<input asp-for="Post.Id" hidden />
<div class="border p-3">
#*<div asp-validation-summary="ModelOnly" class="text-danger"></div>*#
<div class="form-group row">
<h2 class="text-info pl-3">Write new comment</h2>
</div>
<div class="form-group row">
<label asp-for="Comment.Body"></label>
<textarea asp-for="Comment.Body" class="form-control"></textarea>
<span asp-validation-for="Comment.Body" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="col-8 offset-2 row">
<div class="col">
<input type="submit" class="btn btn-info w-100" value="Create" />
</div>
<div class="col">
<a asp-action="Index" class="btn btn-success w-100"><i class="fas fa-sign-out-alt"></i> Back</a>
</div>
</div>
</div>
</form>
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult NewComment(CommentViewModel model)
{
if (ModelState.IsValid)
{
_service.AddNewComment(new Guid(), model.Post.Id, model.Comment.Body);
return RedirectToAction("Index");
}
return View();
}
Models:
public class Post
{
[BsonElement("id")]
public Guid Id { get; set; }
[BsonElement("title")]
[Required]
[MaxLength(64)]
public string Title { get; set; }
[BsonElement("post_body")]
[Required]
[Display(Name = "Post")]
[MaxLength(256)]
public string Body { get; set; }
}
public class Comment
{
[BsonElement("id")]
public Guid Id { get; set; }
[BsonElement("post_id")]
public Guid PostId { get; set; }
[BsonElement("comment_body")]
[Required]
[Display(Name = "Comment")]
[MaxLength(128)]
public string Body { get; set; }
}
When fields are not filled red notification appears, as it should. But once fields are filled and user clicks on create, ModelState.IsValid is still false, and for some reason application tries to reload view, but returns exception on <p>#Model.Post.Title</p> NullReferenceException.
It's extremely weird behavior. Adding new post is almost identical except <input asp-for="Post.Id" hidden /> (since there are no relations to anything else), and it works flawlessly. Here things are glitching out.
Removing if (ModelState.IsValid) and return View(); absolutely fixes the issue. Both validation and POST works. But it should work even with it.
Any clues? All laws of logic say it should work. Otherwise I will be forced to keep it the weird way.
I don't see any weird here. It works according to your code. But you can fix a bug:
if (ModelState.IsValid)
{
_service.AddNewComment(new Guid(), model.Post.Id, model.Comment.Body);
return RedirectToAction("Index");
}
return View(model);
in this case you will not have a null reference exeption.
and by the way I am using this code to find what is invalid in a ModelState:
public static string ValidModelState(ModelStateDictionary modelState)
{
var errorMessage = "";
if (!modelState.IsValid)
{
foreach (var item in modelState.Values)
{
foreach (var modelError in item.Errors)
{
errorMessage += "\n" + "Error: " + modelError.ErrorMessage;
}
}
}
return errorMessage;
}
And using this instead of if (ModelState.IsValid):
var errorMessage = ValidModelState(ModelState);
if( !string.IsNullOrEmpty(errorMessage)).... errorMessage;
It says The Post field is required., except that property does not exist.
You have a [Display(Name = "Post")] on the Post.Body property, And I think the model in the view doesn't have value for Post Body, so the validation failed, and the error message for it is The Post field is required. Go and check it.

ASP.NET MVC EF Core, Property Access in View while on selected ID for different Model

I´m having an issue where I have three models as show below: Person, Competence with PersonCompetence between them. My current controller method gets an Id from previous page and shows that person with a list of this person's Competence and Level. In this View however I want to have a POST for new Competence. So at the same time you are adding a new one and you can see which ones you already have.
With the controller method I have now I can access the PersonCompetence and Competence when showing the list.
I dont have access to the Competence properties for asp-for="Competence" marked ###### in the View for AddComp.
I need the ID of person for POST to right person
I need the CompetenceType for POST to that property
I need PersonCompetence to show the list of current PersonCompetence.
I get that with the current #model CompetenceRadar.Models.Person I only reach Person properties.
I have looked at having a ViewModel with access to all tables with an IEnumerable for each table, but this breaks my current Controller when I search for the Id of the person showing. I have switched the #model in the View, but then I can't access Person ID/name.
So how do I access the Competence properties , list one person and get a list of PersonCompetences for that Person.
Please tell me if you want me to clarify something.
I don't need working code, just something to point me in the right direction for a solution.
Is it a ViewModel?
Can I POST without the asp-forattribute?
Models
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public ICollection<PersonCompetences> PersonCompetences { get; set; }
}
public class PersonCompetence
{
public int ID { get; set; }
public int PersonID { get; set; } // FK
public int CompetenceID { get; set; } // FK
public int Level { get; set; }
public Competece Competence { get; set; }
public Person Person { get; set; }
}
public class Competence
{
public int ID { get; set; }
public string CompetenceType { get; set; }
public string CompetenceCategory { get; set; }
public ICollection<PersonCompetence> PersonCompetences { get; set; }
}
AddComp Kontroller function
public async Task<IActionResult> AddComp(int? id)
{
var person = await _context.Personer
.Include(pk => pk.PersonCompetences)
.ThenInclude(k => k.Competence)
.FirstOrDefaultAsync(m => m.ID == id);
return View(person);
}
View_AddComp View for AddComp
#model CompetenceRadar.Models.Person
<h1>AddComp</h1>
<div class="row">
<form asp-action="AddComp">
<input type="hidden" asp-for="ID" />
<div class="form-group col-sm-4">
<label asp-for="#############" class="control-label col-sm-4"></label>
<input asp-for="#############" class="form-control col-sm-4" />
<span class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-dark" />
</div>
</form>
</div>
#foreach (var item in Model.PersonCompetences)
{
<div class="row py-2 rounded" style="background-color:lightslategray">
<div class="col-sm-3 pt-2">#item.Competence.CompetenceType</div>
<div class="col-sm-1 pt-2">#item.Niva</div>
<div class="col-sm-3 pt-2">#item.Competence.CompetenceCategory</div>
<div class="col-sm-5 d-flex justify-content-end">
<a class="btn btn-dark mr-1" role="button" asp-area="" asp-controller="Competence" asp-action="UpdateLevel" asp-route-id="#item.ID">Update</a>
<a class="btn btn-dark mr-1" role="button" asp-area="" asp-controller="Competence" asp-action="DeleteComp" asp-route-id="#item.CompetenceID">Remove</a>
</div>
</div>
}
Simple anwser is that I needed a ViewModel with all three Models
public class ExampleViewModel {
public Person person { get; set; }
public PersonCompetence personCompetence { get; set; }
public Competence competence { get; set; }}
This alows me to access the different values for ID, CompetenceType and a List for of the current PersonCompetence.
What is ViewModel in MVC?

Why am I getting the 'field is required' validation error even though the field is passed with the form?

I am writing a .NET Core 3.0 MVC Web app. I have a JobApplication model that looks like this:
public class JobApplication
{
[Key]
public int Id{ get; set; }
[Required]
public DateTime CreatedOn { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyy-MM-dd}")]
[Display(Name = "Edited on:")]
public DateTime? EditedOn { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyy-MM-dd}")]
[Display(Name = "Deleted on:")]
public DateTime? DeletedOn { get; set; }
[Required]
public User Applicant { get; set; }
[Required]
public JobOffer JobOffer { get; set; }
[Required]
public ApplicationStatus ApplicationStatus { get; set; }
public string CvHandle { get; set; }
public string AdditionalInformation { get; set; }
}
As you can see, the model holds references to the Job Offer it was created for and the applicant that created the application.
I also have a Controller JobApplicationController that has 2 Create methods:
public async Task<ActionResult> Create(int? id)
{
var offer = await _context.JobOffers.Include(x => x.CreatedFor).FirstOrDefaultAsync(x => x.Id == id.Value);
var user = await _context.Users.FirstOrDefaultAsync(x => x.Name == "Filip");
var model = new JobApplication()
{
JobOffer = offer,
Applicant = user
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([FromForm]JobApplication model)
{
if (!ModelState.IsValid)
{
return View(model);
}
JobApplication ja = new JobApplication
{
...
};
await _context.JobApplications.AddAsync(ja);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
As you can see, one of them returns the Create view, the other gets the model from the view and adds it to the database. I also emphasize that in the first method, the 'JobOffer' and 'Applicant' fields are taken from the database and passed to the form with a model. Here's how the view is set up:
#model HRpest.BL.Model.JobApplication
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
#Html.Hidden("Offer", Model.JobOffer)
#Html.Hidden("Applicant", Model.Applicant)
<div asp-validation-summary="None" class="text-danger"></div>
<div class="form-group">
<label asp-for="CvHandle" class="control-label"></label>
<input asp-for="CvHandle" class="form-control" />
<span asp-validation-for="CvHandle" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AdditionalInformation" class="control-label"></label>
<input asp-for="AdditionalInformation" class="form-control" />
<span asp-validation-for="AdditionalInformation" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
It all seems fine. However, when I try to add an application and submit the form, I get an error:
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"|a72c03fc-4f6501d7781e4a9a.","errors":{"JobOffer":["The JobOffer field is required."],"Applicant":["The Applicant field is required."]}}
I don't understand it. I have both fields in my model. How to make this go away?
Thank you so much in advance.
You need to use HiddenFor() Instead of Hidden().
Read here to understand the difference between the two.
I think is because required too. Because navigation property is complex object and should not be required
public virtual User Applicant { get; set; }
public virtual JobOffer JobOffer { get; set; }
public virtual ApplicationStatus ApplicationStatus { get; set; }
In the view, you are saying, #Html.Hidden("Offer", Model.JobOffer). It should instead be #Html.Hidden("JobOffer", Model.JobOffer) because that's the property name. You wouldn't make that mistake if you were using HiddenFor.
On a more abstract level, you are binding directly to the database entity. It's never a good idea. You should use a model to get the posted values and then copy those values to the entity. You can use AutoMapper to do that automatically for you.

ASP.NET MVC Save List<T> to database

I have a problem saving List items to database. When a field is to be saved(in HTTP POST Create) some of the Lists properties don't have to be saved so I have allowed nullabe for such. However there is one field that I retrieve from the Form and save it. Since the classes are complex I'll restrict the code I'll post here (I'll use one field since the Exception generated is the same).
StringValues is the Class that several List fields inherit from, such as TestPlanChecklist in this case.
public class TestPlanChecklist:StringValues
{
}
public class StringValues
{
[Key]
public int id { get; set; }
public int ChangeRequestsID { get; set; }
public string Value { get; set; }
public ChangeRequests ChangeRequests { get; set; }
}
Part of my Model class
public class ChangeRequests
{
[Required]
public List<TestPlanChecklist> TestPlanChecklist { get; set; }
[Required]
public List<PostActivityChecklist> PostActivityChecklist { get; set; }
[Required]
public List<CMBApproval> CMBApproval { get; set; }
[Required]
public List<TechnicalFeasibility> TechnicalFeasibility { get; set; }
}
In my Create view, this is the code that renders textboxes for TestPlanChecklist field
<div class="form-group">
#Html.LabelFor(model => model.TestPlanChecklist, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<div>
<label class="numbers"> 1 </label>
<input type="text" class="TestPlanChecklist" name="TestPlan" />
<input type="button" value="+" class="roundButton" onclick="add('TestPlanChecklist', 'TestPlan')" />
<input type="button" value="-" class="roundButton" onclick="removeElement('TestPlan')" />
</div>
<div>
<label class="numbers"> 2 </label>
<input type="text" class="TestPlanChecklist" name="TestPlan" />
</div>
<div>
<label class="numbers"> 3 </label>
<input type="text" class="TestPlanChecklist" name="TestPlan" />
</div>
#Html.ValidationMessageFor(model => model.TestPlanChecklist, "", new { #class = "text-danger" })
</div>
</div>
</div>
And HttpPost Create method
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "TestPlanChecklist,PostActivityChecklist,PostActivityChecklist,CMBApproval,TechnicalFeasibility")] ChangeRequests changeRequests,
string[] TestPlan)
{
changeRequests.TestPlanChecklist = new List<TestPlanChecklist>();
foreach (var test in TestPlan)
changeRequests.TestPlanChecklist.Add(new TestPlanChecklist { Value = test });
//SendEmails(TechnicalFeasibility, User.Identity.Name, ChangeUrgency, Priority, DescriptionOfChange, Reason);
//SendEmails(CMBApproval, User.Identity.Name, ChangeUrgency, Priority, DescriptionOfChange, Reason);
if (ModelState.IsValid)
{
db.ChangeRequests.Add(changeRequests);
db.SaveChanges();
return RedirectToAction("List");
}
return View(changeRequests);
}
Kindly note that am just using one field to ask this question, thats why I have removed code for initializing the other List fields.
ModelState.IsValid
returns false. I realize that all the List fields have an error, which states that its impossible to typecast from System.String to the particular class. This is funny since I assign only one field value retrieved from the form and the rest are nullable which makes sense.
Where am I going wrong?
Thanks in advance.
ModelState.IsValid check validation rules, that comes from DataAnnotations in your case [Required] Attribute in ChangeRequests ViewModel.
If you want to use this validation and make some of your List properties nullable you should delete this attribute.
You can check the errors of the validation.
var errores = new List<ModelError>();
foreach (ModelState modelState in ViewData.ModelState.Values)
{
foreach (ModelError error in modelState.Errors)
{
errores.Add(error);
}
}
You get ModelState errors because ChangeRequest has [Required] attributes and gets validated by Mvc. It's unclear to me why you're using that action signature but it's a bad approach. You should rely on ViewModels and not Models directly.
public ActionResult Create(ChangeRequestViewModel viewModel)
{
if(ModelState.IsValid == false) return View(viewModel);
var changeRequest = new ChangeRequest();
foreach(var testPlan in viewModel.TestPlans) {
changeRequest.TestPlanChecklist.Add(new TestPlanChecklist { Value = testPlan }
}
// ...
}

Categories

Resources