MVC 4 Viewmodel fails to databind on postback - c#

Hi there.
For more than a day now I have been unable to update my entity models in my MVC project with values from the post. I have read like 30 related issues here on SO and still I have no idea what is even wrong. I made a small project without Ajax and other disturbances, and now I can watch it fail reliably. I was hoping to run this by you, it is very simple, so perhaps someone more experienced can easily identify my misstake. This is what I came up with:
Models and data:
public class StoryDB : DbContext
{
public DbSet<Story> Stories { get; set; }
}
public class Story
{
[Key]
public int StoryID { get; set; }
public string zestory { get; set; }
}
public class StoryViewModel
{
public Story stry;
}
Controller:
//
// GET: /Story/Create
[HttpGet]
public ActionResult Create()
{
StoryViewModel svm = new StoryViewModel();
svm.stry = new Story();
return View(svm);
}
//
// POST: /Story/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(StoryViewModel svm)
{
if (ModelState.IsValid)
{
db.Stories.Add(svm.stry);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(svm);
}
View:
#model MvcApplication7.Models.StoryViewModel
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Story</legend>
#Html.HiddenFor(model => model.stry.StoryID)
<div class="editor-label">
#Html.LabelFor(model => model.stry.zestory)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.stry.zestory)
#Html.ValidationMessageFor(model => model.stry.zestory)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
This is the HTML it produces:
<form action="/Story/Create" method="post"><input name="__RequestVerificationToken" type="hidden" value="v0wlNDXbhykfe6IKOep6WMSZVteTDNPdgUVkQ8VcW4RW2oo-ufJSdVkN70Y0OHI4if56HrZiGLaGVy9UzlsoTkKXm963ZwRfJsuBZSVs9uQ1" /> <fieldset>
<legend>Story</legend>
<input data-val="true" data-val-number="The field StoryID must be a number." data-val-required="The StoryID field is required." id="stry_StoryID" name="stry.StoryID" type="hidden" value="0" />
<div class="editor-label">
<label for="stry_zestory">zestory</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="stry_zestory" name="stry.zestory" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="stry.zestory" data-valmsg-replace="true"></span>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
</form>
And what happens is that there is no databinding I can notice, and it crashes on trying to save with an error due to the incomming viewmodel's Story stry being null.
I have also tried to create new viewmodel and belonging story and filling them with TryUpdateModel() with the result that empty entries were made into the database.
Possibly relevant:

Try changing the field to a property:
public class StoryViewModel
{
public Story stry { get; set; }
}

Related

The value is not set after POST if use asp-for attribute in MVC Core 3.1

I have simple view model:
public class GogViewModel
{
[Required]
public string Name { get; set; }
public bool IsBeta { get; set; }
public string Result { get; set; }
}
and controller:
[HttpGet]
public IActionResult Index()
{
return View(new GogViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(GogViewModel model)
{
if (ModelState.IsValid)
{
model.Result = DoSomething();
}
return View(model);
}
and View:
#model GogViewModel
<div>
<form method="post">
#Html.AntiForgeryToken()
<div class="form-group row">
<label class="col-sm-2 col-form-label" asp-for="Name">Release name</label>
<div class="col-sm-10">
<input type="text" class="form-control" asp-for="Name" placeholder="paste release name here" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" asp-for="Result">Result</label>
<div class="col-sm-9">
<input type="text" class="form-control-plaintext" readonly="readonly" asp-for="Result" />
<p>#Model.Result</p> <!-- it works here means the value is printed -->
</div>
<div class="col-sm-1">
<button type="button" class="btn btn-outline-info copy float-right" data-target="Result"><i class="far fa-copy"></i></button>
</div>
<div class="clearfix"></div>
</div>
<input type="submit" class="btn btn-success" value="Fix it" />
</form>
</div>
Somehow the result is not output after POST request.
Why ? Where's my mistake ?
It will work in the following way:
Set private setter to Result property => public string Result {get; private set;}
Add method which set value for Result: internal void SetResult(string result) { Result = result; }
In controller, call SetResult like: model.SetResult(...);
and will display in readonly input. I don't know why it works and I don't understand why the value is cleared when put it on input.
I used input with readonly attribute in order to copy to clipboard the output value with javascript.
maybe you need to add something in the input tag like value="#Model.Result"?

Asp.Net MVC Core - Post Action passes old model data to different view

Product Model Class
public class Product
{
public Guid ProductId { get; set; }
public string Name { get; set; }
}
Home Controller:
public class HomeController : Controller
{
[HttpGet]
public IActionResult EditProduct()
{
Product product = new Product()
{
ProductId = Guid.Empty,
Name = "Abc"
};
return View(product);
}
[HttpPost]
public IActionResult EditProduct(Product product)
{
Product productCopy = new Product()
{
ProductId = Guid.NewGuid(),
Name = product.Name + "d"
};
return View("EditProductCopy", productCopy);
}
}
EditProduct View
Note: Auto Generated from Product Model Class,
EditProductCopy is similar, only title is different
#model WebApplicationTest.Models.Product
#{
ViewData["Title"] = "EditProductCopy";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>EditProductCopy</h1>
<h4>Product</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="EditProduct">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProductId" class="control-label"></label>
<input asp-for="ProductId" class="form-control" />
<span asp-validation-for="ProductId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
I call "EditProduct" view. I don't change anything and I click the "Save" button. "EditProductCopy" view is rendered and I expect to see that new ProductId and "Abcd" as Product name. But I see last posted data (ProductId is empty guid and Name is "abc") What is the reason of this problem?
Project Files can be downloaded from this link:
Project Files
Default TagHelper display ModelState value not Model.Just add ModelState.Clear() before you return View:
[HttpPost]
public IActionResult EditProduct(Product product)
{
Product productCopy = new Product()
{
ProductId = Guid.NewGuid(),
Name = product.Name + "d"
};
ModelState.Clear();//add this line...
return View("EditProductCopy", productCopy);
}
Result:
The request is submitted with GET method because method attribute of the form is not specified. So, write method="POST" in the form.

How do I insert multiple rows from a POST

I have a View that contains n number of input fields (the number will vary based on different criteria). the value of each input field needs to be inserted in the database in their own row.
My problem is that only the first input field is inserted into the database. The controller looks like this:
public ActionResult Create([Bind(Include = "Id,MemberId,Rated,Rating")] Rating rating)
{
if (ModelState.IsValid)
{
db.Ratings.Add(rating);
db.SaveChanges();
}
The value of MemberId is the same for each row, but Rated and Rating will be different.
My model looks like this:
[Table("Rating")]
public partial class Rating
{
public int Id { get; set; }
public int? MemberId { get; set; }
public int Rated { get; set; }
[Column("Rating")]
public int Rating { get; set; }
public virtual TeamMember TeamMember { get; set; }
}
The view looks like this:
#model Teamer.Models.Rating
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Rating</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#{
int count = 0;
foreach (var item in ViewBag.MemberId)
{
string rater = Request.Cookies["rater"]["name"].ToString();
string raterId = Request.Cookies["rater"]["raterId"];
if (item.Name.ToLower() != rater.ToLower())
{
if (ViewBag.raterId != null)
{
foreach (var raterid in ViewBag.raterId)
{
<input type="hidden" name="MemberId" value="#raterid" />
}
}
<div class="col-md-10">
<label class="control-label col-md-2" for="#item.Name">#item.Name</label>
<input type="number" name="Rating-#count" value="0" />
<input type="hidden" name="Rated" value="#item.Id" />
</div>
count++;
}
}
}
</div>
<input type="hidden" name="count" value="#count" />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
I'm guessing, I need to loop through the values one by one, but I can't get that to work, so maybe I'm way off on that.
EDIT
OK, so I got it working now with at simple, although maybe a bit ugly, solution.
My controller now looks like this:
public ActionResult Create(Rating rating)
{
int count = int.Parse(Request.Form["count"]);
for (int i = 0; i < count; i++)
{
string test = Request.Form["MemberId"];
rating.MemberId = int.Parse(test);
rating.Rated = int.Parse(Request.Form["Rated-" + i]);
rating.Rating1 = int.Parse(Request.Form["Rating1-" + i]);
db.Ratings.Add(rating);
if (ModelState.IsValid)
{
db.SaveChanges();
}
}
return RedirectToAction("Index");
}
And my View:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Rating</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#{
int count = 0;
string raterId = Request.Cookies["rater"]["raterId"];
foreach (var item in ViewBag.MemberId)
{
string rater = Request.Cookies["rater"]["name"].ToString();
if (item.Name.ToLower() != rater.ToLower())
{
<div class="col-md-10">
<label class="control-label col-md-2" for="#item.Name">#item.Name</label>
<input type="number" name="Rating1-#count" value="0" />
<input type="hidden" name="Rated-#count" value="#item.Id" />
</div>
count++;
}
}
}
</div>
<input type="hidden" name="MemberId" value="#raterId" />
<input type="hidden" name="count" value="#count" />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
It's simple enough, but I get the feeling there's a better, cleaner way. Any suggestions?
I can see a couple of problems, but it the end, you're not posting what is expected by your controller. Your controller is expecting to get a Rating (so Id, MemberId, Rated, and Rating), but you're actually posting something like
MemberId=raterId (repeated by as many ViewBag.raterId you have)
Rating-0=0
Rated=memberX_Id
Rating-1=0
Rated=memberY_Id
Rating-2=0
Rated=memberZ_Id
(...)
Rating-n=0
Rated=memberM_Id
count=(n+1)
If you check your posted data, you should have only MemberId and Rated filled.
In order to achieve what you want, you'll need to have a list of Rating or a different structure on your controller side. Bear in mind, if you want to post an array, it should be something like
MemberId[0]=
Rated[0]=
Rating[0]=
MemberId[1]=
Rated[1]=
Rating[1]=
(...)
MemberId[n]=
Rated[n]=
Rating[n]=
Also, note that MemberId and Rated get overridden in your loops.
The Client is sending an array to controller, binding modification is required. The controller should accept an array or collection of Rating:
public ActionResult Create(Icollection<Rating> ratings)
{
if (ModelState.IsValid)
{
//saving...
}
}
Update:
As one of the good practices, It is better to work with DTO instead of your Database Entity, you might want to create a RatingDto Class:
public partial class RatingDto
{
public int MemberId { get; set; }
public int Rate { get; set; }
}
And not to forget changing View:
<div class="col-md-10">
<input type="hidden" name="MemberId" value="#raterId" />
<input type="number" name="Rate" value="0" />
</div>
Binding in Asp.net MVC works like so, associate html name attribute to property name in C# code.
It`s rather complicated to know what another programmer have expected from his code to do but the Key points are:
Instead of ViewBags and Cookies use Model
Pay attention to how Binding works
Replace Entity with Dto
Use debugger (Google Chrome,Firefox, etc) to check what do you send
to browser
Hope it helps.

ASP.Net MVC Postback from View to Controller shows null values

I've a problem with ViewModel posting back to a controller, but the ViewModel not being mapped correctly from the View to the Controller.
TopicId and Content should contain values, however, when posted back, they do not:
VS Debug:
ViewModels:
public class PostViewModel
{
public int PostId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Author { get; set; }
public DateTime DateOfTopic { get; set; }
}
public class ReplyViewModel
{
public int TopicId { get; set; }
public string Content { get; set; }
}
public class PostListAndReplyVM
{
public List<PostViewModel> PostViewModel { get; set; }
public ReplyViewModel ReplyViewModel { get; set; }
}
View:
#model centreforum.Models.PostListAndReplyVM
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Post</legend>
#Html.HiddenFor(model => model.ReplyViewModel.TopicId)
<div class="editor-label">
#Html.LabelFor(model => model.ReplyViewModel.Content)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.ReplyViewModel.Content)
#Html.ValidationMessageFor(model => model.ReplyViewModel.Content)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Generated HTML:
<form action="/Post/List/7/" method="post"><input name="__RequestVerificationToken" type="hidden" value="xxxxxxxxxxxxx" /> <fieldset>
<legend>Post</legend>
<input data-val="true" data-val-number="The field TopicId must be a number." data-val-required="The TopicId field is required." id="ReplyViewModel_TopicId" name="ReplyViewModel.TopicId" type="hidden" value="7" />
<div class="editor-label">
<label for="ReplyViewModel_Content">Content</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="ReplyViewModel_Content" name="ReplyViewModel.Content" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="ReplyViewModel.Content" data-valmsg-replace="true"></span>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
</form>
As you can see from the generated HTML, the TopicId definitely has a value: value="7"
Can anyone see where the problem is between the form post, and the controller, which is expecting the ReplyViewModel?
Thank you,
Mark
Your input field names are prefixed with ReplyViewModel (because of the model => model.ReplyViewModel.* lambda), so you need to indicate this information to the model binder:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult List([Bind(Prefix = "ReplyViewModel")] ReplyViewModel model)
{
...
}
Alternatively have your List action take the PostListAndReplyVM model:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult List(PostListAndReplyVM model)
{
// obviously only model.ReplyViewModel will be bound here because
// those are the only input fields in your form
...
}
The problem is the fact that your view is typed to PostListAndReplyVM - so it creates names such as ReplyViewModel.Content - but, because your controller action expects a ReplyViewModel, these fields can't be bound (i.e. there is no such thing as ReplyViewModel.ReplyViewModel.Content).
Change your controller action:
public ActionResult List(PostListAndReplyVM reply)
Alternatively - if that's your whole view - just type it to ReplyViewModel instead (and update your HtmlHelper expressions accordingly).
Its null because you bound it to another model
In view
#model centreforum.Models.PostListAndReplyVM
In Action ReplyViewModel
try to bind like
public ActionResult SomeAction(PostListAndReplyVM model)
{
}

Can't get selected drop down value to bind to view model property

I'm having trouble binding the selected value of a drop down list to the correct property in my view model. I can't see what I am doing wrong here. I've put the code that should help show what I'm doing below. I've omitted some things such as the population of the 'AllFolders' property of the view model, as it's just a simple List with an object called ImageGalleryFolder.
Every time the form posts back, the ParentFolderId property is null without fail. This is driving me crazy and I've wasted a lot of time trying to work it out.
Can anyone see something I'm doing wrong?
This is the view model
public class ImageGalleryFolderViewModel
{
[Required]
public string Title { get; set; }
public int Id { get; set; }
public string CoverImageFileName { get; set; }
public HttpPostedFileBase UploadedFile { get; set; }
public string ParentFolderId { get; set; }
public IList<ImageGalleryFolder> AllFolders { get; set; }
}
Here is the view code
#using Payntbrush.Presentation.Demo.MVC3.Areas.Admin
#model Payntbrush.Presentation.Demo.MVC3.Areas.Admin.Models.ImageGalleryFolderViewModel
#{
ViewBag.Title = "Create A New Gallery Folder";
}
<h2>#ViewBag.Title</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm((string)ViewBag.Action + "Folder", "Portfolio", FormMethod.Post, new { Id = "CreateFolder", enctype = "multipart/form-data" }))
{
#Html.ValidationSummary(true)
if(((string)ViewBag.Action).ToLower() == FormConstants.Edit.ToLower())
{
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.CoverImageFileName)
#Html.HiddenFor(m => m.ParentFolderId)
}
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.UploadedFile)
</div>
<div class="editor-field">
<input type="file" name="UploadedFile"/>
#Html.ValidationMessageFor(model => model.UploadedFile)
</div>
{
// Count > 1 is important here. If there is only 1 folder, then we still don't show the drop down
// as a child folder can't have itself as it's own parent.
}
if(#Model.AllFolders.Count > 1)
{
<div class="editor-label">
Choose a parent folder (optional)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.ParentFolderId, new SelectList(Model.AllFolders, "Id", "Title"))
</div>
}
<p>
<input type="submit" value="Save" />
</p>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
I've ommitted my view, but this is what my form looks like when rendered in the browser. The form looks good from what I can see?
<form Id="CreateFolder" action="/SlapDaBass/Portfolio/EditFolder/1" enctype="multipart/form-data" method="post">
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Id" name="Id" type="hidden" value="1" />
<input id="CoverImageFileName" name="CoverImageFileName" type="hidden" value="" />
<input id="ParentFolderId" name="ParentFolderId" type="hidden" value="" />
<div class="editor-label">
<label for="Title">Title</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-required="The Title field is required." id="Title" name="Title" type="text" value="Test" />
<span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="UploadedFile">UploadedFile</label>
</div>
<div class="editor-field">
<input type="file" name="UploadedFile"/>
<span class="field-validation-valid" data-valmsg-for="UploadedFile" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
Choose a parent folder (optional)
</div>
<div class="editor-field">
<select id="ParentFolderId" name="ParentFolderId">
<option value="1">Test</option>
<option value="2">Test 2</option>
</select>
</div>
<p>
<input type="submit" value="Save" />
</p>
</form>
And this is the controller action:
[HttpPost]
public ActionResult EditFolder(int id, ImageGalleryFolderViewModel model)
{
if (ModelState.IsValid)
{
Services.PortfolioService.UpdateFolder(model.MapToDomainModel(), model.UploadedFile);
return Home;
}
return View();
}
change the data type of the ParentFolderId
public class ImageGalleryFolderViewModel
{
[Required]
public string Title { get; set; }
public int Id { get; set; }
public string CoverImageFileName { get; set; }
public HttpPostedFileBase UploadedFile { get; set; }
public int ParentFolderId { get; set; }
public IList<ImageGalleryFolder> AllFolders { get; set; }
}
also use the Html helper for the dropdownlist
<%:
Html.DropDownListFor(
model => model.ParentFolderId ,
new SelectList(
new List<Object>{
new { value = 1 , text = "Test" },
new { value = 2 , text = "Test2" },
new { value = 3 , text = "Test3"}
},
"value",
"text"
)
)
%>
i hope you are strongly typing your view like
public ActionResult EditFolder()
{
return View(new ImageGalleryFolderViewModel());
}
Please refer below link for the bind drop down list. It will be very helpful to you.
ASP.NET MVC - drop down list selection - partial views and model binding
Here if you do not want to create property in model for the List of items, than you can also store it in a ViewData or ViewBag. Please find sample code below.
<%= Html.DropDownList("Category.CategoryId", new SelectList((
IEnumerable<ProductManagement.Models.Category>)ViewData["CategoryList"],
"CategoryId", "CategoryName"))%>
You're creating a Hidden input for ParentFolderId with an empty value. This is probably overriding the value that the DropDownList is trying to post. Remove this line:
#Html.HiddenFor(m => m.ParentFolderId)
you have 2 element for ParentFolderId
one of them is hidden field
#Html.HiddenFor(m => m.ParentFolderId)
second is select element
#Html.DropDownListFor(m => m.ParentFolderId, new SelectList(Model.AllFolders, "Id", "Title"))
and modelbinder bind the first matched element value to model.
You have to remove hidden field

Categories

Resources