Rename HTML name attribute to get data into model - c#

I have problems to pass data from a form using post into my model.
I have the following code:
public ActionResult Edit(int? id)
{
...
return this.View("Edit", Tuple.Create(staff, team);
}
As you can see I am returning a Tuple to the View, because I need to have more than one model. I guess I would normally create a ViewModel, but in this case this would be to much I think.
Is there generally something wrong returning a Tuple, List, or even a Dictionary to a View? Should I always create a ViewModel?
This is the View:
#model Tuple<Staff, List<Team>>
#{
var staff = Model.Item1;
var teams = Model.Item2;
}
#using(Html.BeginForm())
{
...
#Html.LabelFor(model => staff.Foo)
#Html.EditorFor(model => staff.Bar)
}
#using(Html.BeginForm())
{
...
#Html.LabelFor(model => team.Foo)
#Html.EditorFor(model => team.Bar)
}
Anyway, this code renders like so:
<input type="text" ... name="staff.Foo" ... />
and
<input type="text" ... name="team.Foo" ... />
This is my target controller (when I submit form "staff"):
[HttpPost]
public ActionResult Edit([Bind(Include = "foo,bar")] Staff staff)
{
...
this.DbContext.SaveChanges();
...
}
The problem is, that the data will be send via post, but my models keeps beeing empty. I guess this is due to the fact I passed my models as a Tuple to the view.
Even though I change
#Html.EditorFor(model => model.Item1.Foo)
will be
<input type="text" ... name="Item1.Foo" ... />
How can I fix this. I couln't find a solution to rename the name attribute to simply "Foo" instead of "staff.Foo". I guess this would fix the problem. Do I really have to create a ViewModel?
Best regards

If you are only posting one complex property of the model/tuple, you can use the Bind.Prefix property
[HttpPost]
public ActionResult Edit([Bind(Prefix="staff")] Staff model)
This effectively strips the staff. prefix from the properties so staff.Foo becomes Foo and will be bound to class Staff, however I strongly recommend using a view model rather than Tuple (and its actually less code anyway).

Related

How to populate a Model in MVC

So I am trying to populate a model, which needs a userId. So right now, my ActionResult Index method just returns a view which prompts for the user to enter their userId. I need to grab this value and then create the viewmodel and then pass it in back to the view so that way I can do stuff like #Model.blah. I was wondering how I can do that, whether it'd be having two different action result methods, or in general how to populate models when the information you need must be queried first before constructing the viewmodel.
Here is my controller:
public ActionResult Index()
{
// Ask for UserID
return View("~/Views/FingerprintTool/Index.cshtml");
}
public ActionResult Index(int userId)
{
var response = _driver.ListFingerprints(userId);
var model = new FingerprintToolModel()
{
Fingerprints = response.Fingerprints
};
return View(model);
}
And here is my html:
model Models.Tools.FingerprintToolModel
<head>
<script type="text/javascript" src="~/Scripts/FingerprintTool.js"></script>
</head>
<body>
<h1>Fingerprint Tool</h1>
<form id="userIdForm" method="post">
Type in your UserId: <input name="userId" type="number" id="formUserId"/>
<input type="submit"/>
</form>
#if (Model != null)
{
<h1>SUCCESS</h1>
}
</body>
I also have a Javascript file that deals with the submit button be clicked and whatnot.
Here is the Js:
window.onload = function() {
$("#userIdForm")
.submit(function(e) {
e.preventDefault();
if ($("#formUserId").length == 0) {
alert("Invalid UserId");
} else {
listUserFingerprints();
}
});
}
function listUserFingerprints() {
// what to do here
}
Form:
First, update your form. You can use a simple HTML form, or the #Html.BeginForm() helper, like so:
#using Html.BeginForm()
{
#Html.AntiForgeryToken()
<label for="userId">Enter your User ID:</label>
<input type="number" name="userId" id="userId" />
<input type="submit" />
}
By default, your Html.BeginForm creates all the necessary elements and form action etc. If you prefer, you can still use standard HTML. Both do the same job.
Note the AntiForgeryToken. This is so useful when using POST form data (and even get, if you really must).
In your Controller, you can then check against this AntiForgeryToken, to protect against malicious posts or data being injected - see below.
Controller:
Create another method in your Controller, with the same name as your existing one; decorate it with the [HttpPost] attribute.
If you are using an AntiForgeryToken, you need to decorate the method with [ValidateAntiForgeryToken] also.
Like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(int userId)
{
// MVC's DefaultModelBinder is smart enough to map your form
// post values to objects of the correct type, given the name in the form
// Get the data from your repository etc.
var model = GetUser(userId);
// Then return this model to the view:
return View(model);
}
Notice the parameter we are looking for in the method signature matches the name attribute of the input in your form.
MVC's DefaultModelBinder is able to make the connection between the two and map the value(s) of any parameters to form values.
You can also check if your model is null (for example, that userId doesn't exist) and then return an error to the page if so.
I like to use validation errors, but you can also use ViewBag or any other kind of method.
You can do a check and add an error, like this:
// Get the data from your repository etc.
var model = GetUser(userId);
if (model == null)
{
ModelState.AddModelError("", "The user ID you entered cannot be found. Please try again");
}
// Then return this model to the view:
return View(model);
This will add a "generic" model error to the view data, which you can then process in the view. More on that, below.
View:
In order to support your view displaying your model, you need to insert an #model statement at the top of your cshtml file.
Like so:
#model MyNameSpace.Models.User
This tells the view engine what Model type to expect from the Controller. In this case I have used User, but it would be whatever your class is called.
Be sure to use the fully-qualified namespace of your class in order to access it.
Then, in your HTML code, you can access the properties of your model using #Model.YourProperty.
Like this:
...
<div>#Model.Username</div>
<div>#Model.FullName</div>
<ul>
#foreach (var fingerPrint in Model.FingerPrints){
<li>#fingerPrint.WhateverProperty</li>
}
</ul>
...
As you can see, this loops through the FingerPrints (or whatever the property is called on your model object) and prints them out in a <ul>. This is to give you an idea of how to access the data from your model.
It is a good idea to create strongly-typed views like this - as this is the WHOLE idea of MVC in the first place :)
Don't forget to add an if check around the part of the page you're access the #Model data (otherwise you will get a NullReferenceException):
#if (Model != null){
<div id="modelDataInHere">
... // Display the data from your model, nice and pretty-like
</div>
}
Note the casing difference between the declaration and printing the values to the HTML output. I won't go into detail, but this is correct and necessary. Don't mix the two up.
Over to that "Validation Error" (AddModelError) we added in the Controller.
If you're using the Html.BeginForm() helper - like in the example - then you can add a Summary in the form, somewhere. When ModelState.AddModelError() is called in your controller, this populates the view data and this error can be displayed on the page.
Like so:
#using Html.BeginForm()
{
#Html.AntiForgeryToken()
// This puts any "generic" error messages at the top of the form
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
// Original form fields
<label for="userId">Enter your User ID:</label>
<input type="number" name="userId" id="userId" />
<input type="submit" />
}
No need for any Ajax or any JavaScript in here. You're simply using MVC for what it was designed in the first place.
You can use Ajax, however.
I would encourage you to read into it. There is also a similar helper: Ajax.BeginForm(), which renders the form as an Ajax one.
A bit of JS set up is required. And your controller action could return a PartialView, rather than a full-blown one.
Have a read up on using Ajax form posts here: http://eliot-jones.com/2014/09/mvc-ajax
The finished article
Time is marching on, and this post is getting longer.
But for the sake of clarity, your view and controller should look something like below. (I've stuck in a made-up class, so you can see the where the properties come from):
View
#model YourNameSpace.Models.User
... all your other code in here...
<div>
#using Html.BeginForm()
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<label for="userId">Enter your User ID:</label>
<input type="number" name="userId" id="userId" />
<input type="submit" />
}
</div>
#if (Model != null)
{
<!-- If there is a model present, display the data for it: -->
<div>
<div>#Model.Username</div>
<div>#Model.FullName</div>
<ul>
#foreach (var fingerPrint in Model.FingerPrints)
{
<li>#fingerPrint.WhateverProperty</li>
}
</ul>
</div>
}
Controller
public ActionResult Index()
{
// This is your standard "GET" request for "Index"
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(int userId)
{
// MVC's DefaultModelBinder is smart enough to map your form
// post values to objects of the correct type, given the name in the form
// Get the data from your repository etc.
var model = GetUser(userId);
if (model == null)
{
ModelState.AddModelError("", "The user ID you entered cannot be found. Please try again");
}
// Then return this model to the view:
return View(model);
}
Sample model
public class User
{
public string Username { get; set; }
public string FullName { get; set; }
public List<FingerPrint> FingerPrints { get; set; }
}
Sincerely hope this helps you, and I wish you the best in your project.
Any questions, please feel free to ask :)
Seems like you need to work with json and AJAX method:
[HttpPost]
public ActionResult BasePage(int userId)
{
// user ID is binded to userId variable, based on the input name
var model = populate(userId);
// Remember to return the model to the view
return Json(model);
}
The Javascript for calling this would be:
function listUserFingerprints() {
$.post("/FingerprintTool/BasePage", {userId: $("#formUserId").val() }, function(model) {
console.log(model); // do whatever you want with model here
}
}

MVC5 Upload File Request.Files works where controller method arguments do not

I am uploading a file using a simple form. The issue I am having is the controller is given a view model as its only argument (The form is based on this view model). When I try to access the posted file through the view model argument it is null. However, when I access the file using Request.Files["FormUpload.File"] I am able to access the posted file.
I understand that in reality the form is attaching an id FormUpload.File which I can access. What I don't understand is how to use the view model to access the file instead. Are my controller arguments correct?
I've put the relevant bits of code below. Any explanation would be greatly appreciated.
The UploadView View Model:
public class UploadViewModel : ListViewModel
{
public HttpPostedFileBase File { get; set; }
}
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload(UploadViewModel model)
{
HttpPostedFileBase requestFile = Request.Files["FormUpload.File"];
// Reference to View Model File
// model.File = null
// Reference to Request.Files
// requestFile = true
}
View (Consider that FormUpload is an instance of an UploadView view model):
#using (Html.BeginForm("Upload", "RelevantController" , new { enctype = "multipart/form-data" }))
{
#Html.AntiForgeryToken()
<fieldset>
<legend>Upload a file.</legend>
#Html.TextBoxFor(m => m.FormUpload.File, new { type = "file" })
<input type="submit" name="Submit" id="Submit" value="Create" class="btn btn-default" />
</fieldset>
}
Cheers and thank you.
As an aside there are countless other questions that relate to "How do I upload this file. I keep getting null". My current workaround seems to be a popular answer.
The fact that you view has #Html.TextBoxFor(m => m.FormUpload.File, new { type = "file" }) means the model in the view is not UploadViewModel, but rather a model containing a property named FormUpload which is typeof UploadViewModel.
The reason it wont bind is because UploadViewModel does not contain a property named FormUpload which in turn contains a property named File.
You need to either
Change the parameter in the POST method to use the same model as
declared in the view, for example, if its #model MyModel then the
method needs to be public ActionResult Upload(MyModel model), or
Use the Prefix property of the BindAttribute which effectively
strips the prefix when binding - public ActionResult Upload([Bind(Prefix="FormUpload")]UploadViewModel model)

When submitting MVC 5 view, data from partial view is missing [duplicate]

This question already has answers here:
getting the values from a nested complex object that is passed to a partial view
(4 answers)
Closed 6 years ago.
This question has been asked but none could solve my problem.
The problem is missing or null data from the partial view is not submittied (POST) along with the main view data.
I have a typed partial view called _Address.cshtml that I include in another view called Site.cshtml.
The typed site view binds to a view model called SiteEditModel.cs
public class SiteEditModel
{
...properties
public AddressEditModel Address {get;set;}
public SiteEditModel()
{
Address = new AddressEditModel();
}
}
The Site view has a form:
#model Insight.Pos.Web.Models.SiteEditModel
...
#using ( Html.BeginForm( "Edit", "Site", FormMethod.Post ) )
{
#Html.HiddenFor( m => m.SiteId )
...
#Html.Partial( "~/Views/Shared/Address.cshtml", this.Model.Address )
...
#Html.SaveChangesButton()
}
The partial Address view is just a bunch of #Html... calls that bind to the Address model.
#model Insight.Pos.Web.Models.AddressEditModel
#{
Layout = null;
}
<div>
#Html.HiddenFor(...)
#Html.HiddenFor(...)
#Html.HiddenFor(...)
#hmtl.LabelFor(...)
</div>
In the controller action Edit I can see the SiteEditModel is populated correctly, the Address property of that model is not.
Where do I go wrong?
Thank you so much.
http://davybrion.com/blog/2011/01/prefixing-input-elements-of-partial-views-with-asp-net-mvc/
#Html.Partial("~/Views/Shared/_Address.cshtml", Model.Address, new ViewDataDictionary
{
TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "Address" }
})
The key to fix this is with naming of the partialviews input-elements. The Render partial dont know it's a part of something bigger.
I've make an simple example on how you can fix this in a way that you can have multiple Addresses using the same partial view:
public static class HtmlHelperExtensions
{
public static MvcHtmlString PartialWithPrefix(this HtmlHelper html, string partialViewName, object model, string prefix)
{
var viewData = new ViewDataDictionary(html.ViewData)
{
TemplateInfo = new TemplateInfo
{
HtmlFieldPrefix = prefix
}
};
return html.Partial(partialViewName, model, viewData);
}
}
And use this extensions in the view like this:
#using (Html.BeginForm("Edit", "Site", FormMethod.Post))
{
#Html.HiddenFor(m => m.SiteId)
#Html.PartialWithPrefix("_Adress", this.Model.Address, "Address")
<input type="submit" />
}
You can of course make this a bit more fancy with expressions and reflection but that's another question ;-)
Your SiteEditModel Address property is not marked as public, change it to this instead:
public AddressEditModel Address {get;set;}
I would also change your partial to use SiteEditModel instead:
#model Insight.Pos.Web.Models.SiteEditModel
#{
Layout = null;
}
<div>
#Html.HiddenFor(m => m.Address.FooProperty)
...
</div>
This would mean that your properties would end up being named correctly in order for the model binder to pick them up.
Using the above example it would be Name"=Address.FooProperty".
As I remember correctly, the problem is that Html.Partial doesn't populate inputs names correctly. You should have something like:
<input id="Address.Street" name="Address.Street" />
but I assume you have following HTML:
<input id="Street" name="Street" />
You have few solutions:
Insert input name manually:
#Html.HiddenFor(x => x.Street, new { Name = "Address.Street" })
Use Html.EditorFor()
Override names resolving in Html.Partial()
The downside of first solution is that you are hardcoding property name, what isn't ideal. I'd recommend using Html.EditorFor() or Html.DisplayFor() helpers, cause they populate inputs names correctly.
Model binder could not bind child models correctly if you are populating them in partial view. Consider using editor templates instead which is implemented for this reason.
Put your AddressEditModel.cshtml file in \Views\Shared\EditorTemplates\ folder and in your main view use like this:
#using ( Html.BeginForm( "Edit", "Site", FormMethod.Post ) )
{
// because model type name same as template name MVC automatically picks our template
#Html.EditorFor(model=>model.Address)
// or if names do not match set template name explicitly
#Html.EditorFor(model=>model.Address,"NameOfTemplate")
}

ViewModel does not update Model correctly on submit

I am trying to use a strongly-typed ViewModel and find that when I replace the Model class with a ViewModel, changes are not submitted correctly from the Edit template. Whereas just using the straight Model class in the View, edits happen successfully. My model class is MaterialDefinition and the ViewModel class is MaterialDefinitionViewModel as shown below. I've updated the edit template correctly to reference the ViewModel but as I say editing just does not work. I'm using VS2013 and MVC4. Any ideas, someone?
First the ViewModel class...
public class MaterialDefinitionViewModel
{
// Properties
public MaterialDefinition Definition { get; private set; }
// Constructor
public MaterialDefinitionViewModel(MaterialDefinition def)
{
Definition = def;
}
}
and now some code from the View...
<div class="editor-field">
#Html.EditorFor(model => model.Definition.mdDescription)
#Html.ValidationMessageFor(model => model.Definition.mdDescription)
</div>
<p>
<input type="submit" value="Save" />
</p>
Finally discovered the answer to this after much searching...
[HttpPost]
public ActionResult Edit(string id, FormCollection collection)
{
MaterialDefinition def = repository.GetMaterialDefinition(id);
UpdateModel(def, "Definition");
//UpdateModel(def);
repository.Save();
return RedirectToAction("Details", new { id = def.mdID });
}
Turns out there is a not-so-obvious overload for the UpdateModel method that takes a prefix "Name" property. This is the name of the original encapsulated model class inside the ViewModel. Corrected code fragment from the Controller's Edit Post method is shown above.

Update a Model and a Collection of Models on a Single View (MVC4)

I've been working on an MVC 4 Application and have run into a problem when attempting to update Models in a ViewModel.
My ViewModel (detailed below) contains one ComplexObjectOne and a List<ComplexObjectTwo>.
My GET ActionResult successfully populates the ViewModel from a database and everything displays correctly on my View.
The problem is encountered when attempting to pass the ComplexObjectOne and List<ComplexObjectTwo> to the POST ActionResult.
The ComplexObject is passed correctly but everything I've tried fails pass the List<ComplexObjectTwo> collection.
My ComplexModelOne Model
public class Test
{
public int Id {get;set;}
public string Result {get;set;}
public virtual ICollection<TestResult> TestResults {get;set;}
}
My ComplexModelTwo Model
public class TestResult
{
public int Id {get;set;}
public string Result {get;set;}
public string Comment {get;set;}
public virtual Test Test{get;set;}
}
My ViewModel
public class TestingViewModel
{
public TestingViewModel()
{
if(TestResults == null)
{
TestResults = new List<TestResult>();
}
}
public Test Test {get;set;}
public IEnumerable<TestResult> TestResults {get;set;}
}
My Edit() GET ActionResult
public ActionResult Edit(int id = 0)
{
var viewModel = new TestingViewModel();
Test test = testRepo.GetTestById(id);
var results = test.TestResults;
viewModel.Test = test;
viewModel.TestResults = results;
return View(viewModel);
}
My Edit() POST ActionResult
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(TestingViewModel model)
{
// do update - left out for brevity
}
My Edit.cshtml View
#model Namespace.Models.ViewModels.TestingViewModel
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.EditorFor(model => model.Test, "TestHeader")
<table>
<tr>
<th>Test</th>
<th>Result</th>
<th>Comment</th>
</tr>
#Html.EditorFor(model => model.TestResults, "TestResults")
</table>
<input type="submit" value="Update"/>
}
Within my View I do use a couple of EditorTemplates to display the property fields.
Any assistance, comments, or suggestions will be much appreciated. I'd like to be able to accomplish updating these entities on a single page instead of multiple pages which I resorted to in the Create() steps.
Thank you,
Patrick H. (stpatrck)
Replace:
#Html.EditorFor(model => model.TestResults, "TestResults")
with:
#Html.EditorFor(model => model.TestResults)
and then rename your EditorTemplates/TestResults.cshtml editor template to EditorTemplates/TestResult.cshtml (notice the missing s) and inside replace the model declaration from:
#model IEnumerable<TestResult>
to:
#model TestResult
Now obviously this will lead to getting rid of any for or foreach loops you might have written in this editor template because now ASP.NET MVC will automatically invoke the template for each element of the collection.
So for example:
#foreach (var item in Model)
{
#Html.EditorFor(x => item.SomeProperty)
}
will simply become:
#Html.EditorFor(x => x.SomeProperty)
Now look at the generated markup and notice the difference in the names of your input fields. Before you had:
<input type="text" name="item.SomeProperty" value="foo" />
and now you have:
<input type="text" name="TestResults[0].SomeProperty" value="foo" />
Now when you submit the form to the POST action the default model binder will be able to successfully bind the collection because now the naming convention is respected. You can read more about this convention in the following blog post.
Also you have circular references in your object graph which cannot be successfully serialized and model bound. You should use view models in order to break this circular dependency.

Categories

Resources