MVC Model child object null on HTTP post - c#

Hope someone can help me. I am new to MVC, coming from a winforms/console/vb6background.
Apologies if this has already been answered, I am stuggling to understand how I can resolve the below issue.
I have a view model :
public class testvm
{
public int id { get; set; }
public DateTime date { get; set; }
public student studentID { get; set; }
public testvm() { }
public testvm (student s)
{
studentID = s;
}
}
I am pre-populating the student child object of this ViewModel before it is passed to the view.
Student Model :
public class student
{
[Key]
public int ID { get; set; }
public string Name { get; set; }
}
The problem I have is when the model is returned to the create HTTP post method the student child object is blank.
The controller code :
// GET: testvms/Create
public ActionResult Create(int sId)
{
student a = db.students.Find(sId);
testvm b = new testvm(a);
return View(b);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "id,date,student")] testvm testvm)
{
if (ModelState.IsValid)
{
db.testvws.Add(testvm);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(testvm);
}
View code:
#model WebApplication2.Models.testvm
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>testvm</h4>
<hr />
#Html.HiddenFor(model => model.studentID.ID)
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.date, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.date, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.date, "", new { #class = "text-danger" })
</div>
</div>
<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>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
The model object on the view is populated with the student information. When this is passed back to Create POST controller the student child object is null!
Can somebody please advise where I am going wrong or of the correct way to achieve this?
My application will have many forms that will all need to be pre-populated with student information. Each student will have many forms that will need to be filled out for them.
Many thanks in advance,
Rob

For every property in domain model (in your case testvm) you must have an EditorFor or Input element (like TextBoxFor or so) on your view(or HiddenFor for ID or other non user ui data).It may be a pain binding nested models in MVC as the DefaultModelBinder may not be able to bind whole object.However it would be safer approach to expose only the required properties on view like
#Html.HiddenFor(model => model.studentID.ID)
#Html.HiddenFor(model => model.studentID.Name)
and later on Controller Side
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
var originalobj=db.get //get fresh copy from data store
originalobj.Name=testvm.Name;
// ...other properties
//perform required operations on originalobj
}
you may use AutoMapper for this Purpose as
Mapper.CreateMap<testvm,testvm>();
originalobj=Mapper.Map<testvm,testvm>(testvm,originalobj);
you may find more information about Automapper on :
https://github.com/AutoMapper/AutoMapper/wiki/Getting-started

Your property name is called studentId (even though standard C# property naming convention dictates that it should have been called StudentId):
public student studentID { get; set; }
But in your Bind attribute you seem to have specified some student property which doesn't really exist on your view model:
[Bind(Include = "id,date,student")]
So you probably want to get rid of this Bind attribute from your controller action:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
...
}
Also note that you only have a hidden field for the student id inside your form:
#Html.HiddenFor(model => model.studentID.ID)
You don't have a corresponding hidden field for the student name property, so it will never be posted back to your controller action.

Your attribute [Bind(Include = "id,date,student")] should include the names of the properties that you want to be set, student isn't in your model, but studentID is, they have to match.
You don't have to explicitly specify all of the field names that you want to be bound to your model, by default they will be bound anyway unless you tell the binder NOT to bind it by using [Bind(Exclude = "id,date,student")]. Therefore as it currently stands, I'd recommend removing your Include attribute to ease maintenance unless there is an important reason for using it and simply ensure that the models that you bind to only include the values you need.
Secondly, you have to make sure that any values that you are posting back from a form in your view have the same parameter names and are structured the same as the ones that you want to be bound to the request model.
This:
#Html.HiddenFor(model => model.studentID.ID)
Is not the same as:
#Html.HiddenFor(model => model.studentID)

Related

DropDownListFor returns selected value always 0

I have a code where I try to bind data from a Model to DropDownListFor and it successfully displays the drop down in the View when executed but it doesn't return the selected value except for 0 always. I have checked and tried numerous solutions posted here but it doesn't seem to work out.
AdminController.cs (Create() action for the Create.cshtml View and ProductSave() Form action)
public ActionResult Create()
{
var categoryList = _context.Categories.ToList();
var viewModel = new CreateViewModel
{
Categories = categoryList,
};
return View(viewModel);
}
[HttpPost]
public ActionResult ProductSave(CreateViewModel viewModel)
{
return Content(viewModel.SelectedId.ToString());
}
CreateViewModel.cs
public class CreateViewModel
{
public IEnumerable<Category> Categories { get; set; }
public int SelectedId { get; set; }
public Product Product { get; set; }
}
Create.cshtml (I only included the necessary fields for the View here)
#using (Html.BeginForm("ProductSave", "Admin",null,FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="default-form-wrap style-2">
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.Product.Category_Id, new { #class = "control-label"})
<div class="nice-select single-select">
#Html.DropDownListFor(model => model.SelectedId,new SelectList(Model.Categories,"Id","CategoryName"),"Select Category", new { #class = "form-control" } )
#Html.ValidationMessageFor(model => model.Product.Category_Id, "", new { #class = "text-danger" })
</div>
</div>
<div class="row text-center">
<div class="col">
<input type="submit" value="Create" class="btn btn-base" />
</div>
</div>
</div>
When selected a Category the value is changed accordingly
Screenshot
I tried return Content() in the form action method to see the output from the bound viewmodel after I hit submit button
return Content(viewModel.SelectedId.ToString());
But here's what I get
Screenshot
I just want to get the output corresponding to the selected list item.
Looking at your HTML the culprit is probably:
<div class="nice-select single-select">
The DropdownListFor should be generating a <select> element with an ID value that MVC would resolve back to populate the model on the return trip. Your HTML is generating a <ul>.
You could remove that outer <div> or the class definitions and it should work. Getting that "nice-select" to work might take some reading around what it is and interacting with MVC tied controls to get it compatible with the form postback. You might need to use some Javascript and an #Html.HiddenFor so when a value is selected from the <ul> the hidden input that is tied to the model is updated and available for the post-back.

Send one Model from ViewModel back to controller

I am using a ViewModel to pass various models to the view. Now in a form, I would like to return one model (ResetTypeSingleModel) out of this ViewModel but the controller’s method can only expect the ViewModel:
My ViewModel:
public class ResetViewModel {
public IEnumerable<ResetTypeModel> TableResetType {get; set;}
public ResetTypeSingleModel SingleResetType {get; set;}
}
My controller (of course I changed it to ResetTypeSingleModel):
public IActionResult ResetDevice([FromForm] ResetViewModel rtm){ }
My view:
#using (Html.BeginForm("ResetDevice", "Sccm", FormMethod.Post, new { id = "form_reset" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.DropDownListFor(model=> model.SingleResetType.resettype_id, new SelectList(Model.TableResetType, "resettype_id" , "resettype_name"), "Please select", new { #CssClass="ddl_resettype", #id="ddl_resettype", #class = "form-control" })
<input type="submit" value="Reset" class="btn btn-primary" />
}
How can I accomplish that I submit to the controller only this model and not the complete viewModel?
Thanks
Stephan
Update:
So I tried to change the names of the fields but then the pattern etc. does not work anymore:
#{ string name = #Html.NameFor(model => Model.SingleResetType.hostname).ToString().Split(".").Reverse().ToList()[0];}
#Html.LabelFor(model => model.SingleResetType.hostname)
#Html.TextBox(name, null, new { #class = "form-control", #id=name, name =name, })
So I guess the only way is to use javascript

Refreshing a partial view on button click without reloading the page C# [duplicate]

The problem I will be describing is very similar to ones I already found (e.g. this post with nearly identical name) but I hope that I can make it into something that is not a duplicate.
I have created a new ASP.NET MVC 5 application in Visual Studio. Then, I defined two model classes:
public class SearchCriterionModel
{
public string Keyword { get; set; }
}
public class SearchResultModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
Then I created the SearchController as follows:
public class SearchController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult DisplaySearchResults()
{
var model = new List<SearchResultModel>
{
new SearchResultModel { Id=1, FirstName="Peter", Surname="Pan" },
new SearchResultModel { Id=2, FirstName="Jane", Surname="Doe" }
};
return PartialView("SearchResults", model);
}
}
as well as views Index.cshtml (strongly typed with SearchCriterionModel as model and template Edit) and SearchResults.cshtml as a partial view with model of type IEnumerable<SearchResultModel> (template List).
This is the Index view:
#model WebApplication1.Models.SearchCriterionModel
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>SearchCriterionModel</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.Keyword, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Keyword, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Keyword, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="button" id="btnDisplaySearchResults" value="Search" onclick="location.href='#Url.Action("DisplaySearchResults", "SearchController")'" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
<div id="searchResults">
</div>
As you can see, I added a div with id="searchResults" below the standard template and edited the button. What I want is to display the partial view SearchResults.cshtml in the div on the bottom, but only after the button is clicked. I have succeeded in showing a partial view there by using #Html.Partial("SearchResults", ViewBag.MyData), but it is rendered when the parent view is loaded for the first time and I set ViewBag.MyData in the Index() method already, which is not what I want.
Summary: On clicking the button, I will obtain some List of SearchResultModel instances (via database access) and then the partial view should be rendered, using this newly obtained data as model. How can I accomplish this? I already seem fail at the first step, that is reacting to the button click with the above code. Right now, I navigate to the URL ~/Search/DisplaySearchResults, but of course there's nothing there and no code-behind method is called.
In traditional ASP.NET I'd just have added a server-side OnClick handler, set the DataSource for a grid and show the grid. But in MVC I already fail with this simple task...
Update: Changing the button to #Html.ActionLink I can finally enter the controller method. But naturally since it returns the partial view, it's displayed as the whole page content. So the question is: How do I tell the partial view to be rendered inside a specific div on the client side?
Change the button to
<button id="search">Search</button>
and add the following script
var url = '#Url.Action("DisplaySearchResults", "Search")';
$('#search').click(function() {
var keyWord = $('#Keyword').val();
$('#searchResults').load(url, { searchText: keyWord });
})
and modify the controller method to accept the search text
public ActionResult DisplaySearchResults(string searchText)
{
var model = // build list based on parameter searchText
return PartialView("SearchResults", model);
}
The jQuery .load method calls your controller method, passing the value of the search text and updates the contents of the <div> with the partial view.
Side note: The use of a <form> tag and #Html.ValidationSummary() and #Html.ValidationMessageFor() are probably not necessary here. Your never returning the Index view so ValidationSummary makes no sense and I assume you want a null search text to return all results, and in any case you do not have any validation attributes for property Keyword so there is nothing to validate.
Edit
Based on OP's comments that SearchCriterionModel will contain multiple properties with validation attributes, then the approach would be to include a submit button and handle the forms .submit() event
<input type="submit" value="Search" />
var url = '#Url.Action("DisplaySearchResults", "Search")';
$('form').submit(function() {
if (!$(this).valid()) {
return false; // prevent the ajax call if validation errors
}
var form = $(this).serialize();
$('#searchResults').load(url, form);
return false; // prevent the default submit action
})
and the controller method would be
public ActionResult DisplaySearchResults(SearchCriterionModel criteria)
{
var model = // build list based on the properties of criteria
return PartialView("SearchResults", model);
}
So here is the controller code.
public IActionResult AddURLTest()
{
return ViewComponent("AddURL");
}
You can load it using JQuery load method.
$(document).ready (function(){
$("#LoadSignIn").click(function(){
$('#UserControl').load("/Home/AddURLTest");
});
});
source code link

How can I pass hidden field value from view to controller ASP.NET MVC 5?

I am trying to pass hidden field value from view to controller by doing the following
#Html.HiddenFor(model => model.Articles.ArticleId)
and also tried
<input type="hidden" id="ArticleId" name="ArticleId" value="#Model.Articles.ArticleId" />
On both instances the value of ArticleId is 0 but when i use TextboxFor i can see the correct ArticleId, please help
Here it is
View
#model ArticlesCommentsViewModel
....
#using (Html.BeginForm("Create", "Comments", FormMethod.Post))
{
<div class="row">
<div class="col-xs-10 col-md-10 col-sm-10">
<div class="form-group">
#Html.LabelFor(model => model.Comments.Comment, new { #class = "control-label" })
#Html.TextAreaFor(m => m.Comments.Comment, new { #class = "ckeditor" })
#Html.ValidationMessageFor(m => m.Comments.Comment, null, new { #class = "text-danger"})
</div>
</div>
</div>
<div class="row">
#*#Html.HiddenFor(model => model.Articles.ArticleId)*#
<input type="hidden" id="ArticleId" name="ArticleId" value="#Model.Articles.ArticleId" />
</div>
<div class="row">
<div class="col-xs-4 col-md-4 col-sm-4">
<div class="form-group">
<input type="submit" value="Post Comment" class="btn btn-primary" />
</div>
</div>
</div>
}
Controller
// POST: Comments/Create
[HttpPost]
public ActionResult Create(CommentsViewModel comments)//, int ArticleId)
{
var comment = new Comments
{
Comment = Server.HtmlEncode(comments.Comment),
ArticleId = comments.ArticleId,
CommentByUserId = User.Identity.GetUserId()
};
}
Model
public class CommentsViewModel
{
[Required(ErrorMessage = "Comment is required")]
[DataType(DataType.MultilineText)]
[Display(Name = "Comment")]
[AllowHtml]
public string Comment { get; set; }
public int ArticleId { get; set; }
}
ViewModel
public class ArticlesCommentsViewModel
{
public Articles Articles { get; set; }
public CommentsViewModel Comments { get; set; }
}
The model in the view is ArticlesCommentsViewModel so therefore the parameter in your POST method must match. Your use of
#Html.HiddenFor(model => model.Articles.ArticleId)
is correct, but you need to change the method to
[HttpPost]
public ActionResult Create(ArticlesCommentsViewModel model)
and the model will be correctly bound.
As a side note, your ArticlesCommentsViewModel should not contain data models, and instead should contain only those properties you need in the view. If typeof Articles contains properties with validation attributes, ModelState would be invalid because your not posting all properties of Article.
However, since CommentsViewModel already contains a property for ArticleId, then you could just use
#Html.HiddenFor(model => model.Comments.ArticleId)
and in the POST method
[HttpPost]
public ActionResult Create([Bind(Prefix="Comments")]CommentsViewModel model)
to effectively strip the "Comments" prefix
In your controller, you need to pass the hidden value with the model,
for example, if you have a userId as a hidden value, in your Page you add:
#Html.HiddenFor(x => x.UserId)
In your model of course you would already have UserId as well.
In your controller, you need the model as a parameter.
public async Task<ActionResult> ControllerMethod(YourViewmodel model) { model.UserId //this should be your HiddenValue
I guess your model have another class called Articles inside CommentsViewModel.Change your controller function for accessing the ArticleId accordingly.
[HttpPost]
public ActionResult Create(CommentsViewModel comments)//, int ArticleId)
{
var comment = new Comments
{
Comment = Server.HtmlEncode(comments.Comment),
ArticleId = comments.Articles.ArticleId, // Since you are using model.Articles.ArticleId in view
CommentByUserId = User.Identity.GetUserId()
};
}
In my case, I didn't put the hidden input in the form section, but out of form, so it's not send to backend. Make sure put hidden input inside the form.
Also make sure name attribute is specified on the hidden field. Element's "id" is often used on client side but "name" on server side.
<input type="hidden" value="#ViewBag.selectedTraining" id="selectedTraining"
name="selectedTraining" />
In my case, I was passing a couple of fields back and forth between controllers and views. So I made use of hidden fields in the views.
Here's part of the view. Note a controller had set "selectedTraining" and "selectedTrainingType" in the ViewBag to pass to the view. So I want these values available to pass on to a controller. On the hidden tag, the critical thing is set to the "name" attribute. "id" won't do it for you.
#using (Html.BeginForm("Index", "ComplianceDashboard"))
{
<input type="hidden" value="#ViewBag.selectedTraining" id="selectedTraining" name="selectedTraining" />
<input type="hidden" value="#ViewBag.selectedTrainingType" id="selectedTrainingType" name="selectedTrainingType" />
if (System.Web.HttpContext.Current.Session["Dashboard"] != null)
{
// Show Export to Excel button only if there are search results
<input type="submit" id="toexcel" name="btnExcel" value="Export To Excel" class="fright" />
}
<div id="mainDiv" class="table">
#Html.Grid(Model).Columns(columns =>
Then back on the controller:
// POST: Dashboard (Index)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(string excel)
{
string selectedTraining, selectedTrainingType;
selectedTraining = Request["selectedTraining"];
selectedTrainingType = Request["selectedTrainingType"];
Or can put the requests as parameters to the method: public ActionResult Index(string excel, string selectedTraining, string selectedTrainingType)

ASP.NET MVC3 Automapper Viewmodel/Model View validation

(Again, an MVC validation question. I know, I know...)
I'd like to use AutoMapper (http://automapper.codeplex.com/) to validate fields in my Create Views for fields that are not in my database (and thus not in my DataModel).
Example: I have an Account/Create View for users to create a new account and I want both a Password and ConfirmPassword field so users have to enter their password twice for confirmation.
The Account table in the database looks like this:
Account[Id(PK), Name, Password, Email]
I've generated an ADO.NET Entity Data Model and from that, I generated the Models using an ADO.NET Self-Tracking Entity Generator.
I've also written a custom AccountViewModel for validation annotations like [Required].
So, to summarize, this is my project structure:
Controllers:
AccountController
Models:
Database.edmx (auto-generated from database)
Model.Context.tt (auto-generated from edmx)
Model.tt (auto-generated from edmx)
AccountViewModel.cs
Views:
Account
Create.cshtml
The code of my AccountViewModel looks like this:
public class AccountViewModel
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Password { get; set; }
[Required]
[Compare("Password")]
public string ConfirmPassword { get; set; }
}
Now, my Create View looks like this:
#model AutoMapperTest.Models.Account
<script src="#Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
<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())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Account</legend>
<div class="editor-label">
Name
</div>
<div class="editor-field">
#Html.TextBox("Name")
#Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
Email
</div>
<div class="editor-field">
#Html.TextBox("Email")
#Html.ValidationMessageFor(model => model.Email)
</div>
<div class="editor-label">
Password
</div>
<div class="editor-field">
#Html.TextBox("Password")
#Html.ValidationMessageFor(model => model.Password)
</div>
<div class="editor-label">
Confirm your password
</div>
<div class="editor-field">
#Html.TextBox("ConfirmPassword");
#Html.ValidationMessageFor(model => model.ConfirmPassword)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
My code fails because my Model does not contain the ConfirmPassword field of course.
Now, a little bird whispered to me the AutoMapper could fix that for me. But I can't figure it out... Can someone please tell me what I have to do to make this work? My AccountController looks like this now:
private readonly AccountViewModel _viewModel = new AccountViewModel();
private readonly DatabaseEntities _database = new DatabaseEntities();
//
// GET: /Account/Create
public ActionResult Create()
{
Mapper.CreateMap<AccountViewModel, Account>();
return View("Create", _viewModel);
}
//
// POST: /Account/Create
[HttpPost]
public ActionResult Create(AccountViewModel accountToCreate)
{
try
{
if (ModelState.IsValid)
{
var newAccount = new Account();
Mapper.Map(accountToCreate, newAccount);
_database.Account.AddObject(newAccount);
_database.SaveChanges();
}
return RedirectToAction("Index");
}
catch
{
return View();
}
}
But this doesn't work... (Got the example from http://weblogs.asp.net/shijuvarghese/archive/2010/02/01/view-model-pattern-and-automapper-in-asp-net-mvc-applications.aspx)
Can anyone please enlighten me on this matter? Thank you very much, and my apologies for the wall of text and the hundreds of questions about the same subject...
Few remarks about your code:
Your view is strongly typed (#model declaration) to the Account model whereas it should be typed to the AccountViewModel view model (there is no point in declaring a view model if you don't use it in the view).
AutoMapper is not used for validation, only for converting between types
You don't need to declare a readonly field for your view model (AccountViewModel) inside the controller. You could instantiate the view model inside the GET action and leave the default model binder instantiate it as action argument for the POST action.
You should do the AutoMapper configuration (Mapper.CreateMap<TSource, TDest>) only once for the entire application (ideally in Application_Start) and not inside a controller action
There is no Email field on your view model which might be the reason for the update to fail (especially if this field is required in your database)
So here's how your code might look like:
public ActionResult Create()
{
var model = new AccountViewModel();
return View("Create", model);
}
[HttpPost]
public ActionResult Create(AccountViewModel accountToCreate)
{
try
{
if (ModelState.IsValid)
{
var newAccount = Mapper.Map<AccountViewModel, Account>(accountToCreate);
_database.Account.AddObject(newAccount);
_database.SaveChanges();
}
return RedirectToAction("Index");
}
catch
{
return View();
}
}
replace the first line of you view with
#model AutoMapperTest.AccountViewModel
Also you only need to call Mapper.CreateMap once for app lifetime (e.g. at app start)

Categories

Resources