ViewModel does not update Model correctly on submit - c#

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.

Related

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)

Rename HTML name attribute to get data into model

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).

MVC3 : Overriding the Model values in HTTPPost action method

I am trying to learn MVC3. I have used TextBoxFor HTML helper control to persist the value over multiple post backs.
Surprisingly the value is persisting, but not reflecting in the view.
My Model class looks like this :
public class FileViewModel
{
public string FileName { get; set; }
public string ValidationMsg { get; set; }
}
My Actions methods in controller looks something like this :
public ActionResult DemoFormElements()
{
return View();
}
[HttpPost]
public ActionResult DemoFormElements(FileViewModel fVM)
{
fVM.FileName = "Overridden Text" + fVM.FileName ;
return View(fVM);
}
My View looks like this :
#using (#Html.BeginForm())
{
if (Model != null)
{
#Html.LabelFor(b => b.FileName)
#Html.TextBoxFor(n => n.FileName, Model.FileName)
}
else
{
#Html.LabelFor(b => b.FileName)
#Html.TextBoxFor(n => n.FileName)
}
<input type="submit" id="SubmitBtn" value="Ok" />
}
When I post back by clicking the OK button, I am able to get the value what I entered in the
textbox, but in the controller I am trying to append that value to "Hi" & expecting the appended value in my view, which is not happening...
I am seeing the value of the control is persisting (whatever I ve entered), but not changing :(
Please help me with giving some clue if its an expected behavior or I m doing any mistake here?
There are a couple of problems with this. You can't overwrite the values in the model during a postback because MVC will override that and retain the old values in order to redisplay them in the case of an error. You also should not, generally speaking, be returning a view from the POST handler, you should use a PRG (Post - Redirect - Get) pattern. That's done so that if the user clicks Refresh in their browser it doesn't POST again. So, having said all that, I would change the controller as follows:
public ActionResult DemoFormElements()
{
var viewModel = new FileViewModel();
if( TempData.ContainsKey( "UpdatedFilename" )
{
viewModel = TempData["UpdatedFilename"];
}
return View( viewModel );
}
[HttpPost]
public ActionResult DemoFormElements(FileViewModel fVM)
{
TempData["UpdatedFilename"] = "Overridden Text" + fVM.FileName;
return RedirectToAction( "DemoFormElements" );
}
This will also simplify your View because you don't have to do that null check for model, you will always have a model.
#using (#Html.BeginForm())
{
#Html.LabelFor(model => model.FileName)
#Html.TextBoxFor(model => model.FileName)
<input type="submit" id="SubmitBtn" value="Ok" />
}
The straight forward solutions to your problem could be:
In controller Post method, use ModelState.Remove("[Mode's Property Name]") before it is assigned new value with the Controller. Here it should be : ModelState.Remove("FileName");
Or Use ModelState.Clear(): this forces the page to forget all previous values & clears all the previous entries.
Or on the view page, change Html.TextBoxFor() to Html.TextBox() for the particular model property.
Or use PRG pattern : (Post-Redirect-Get pattern) as suggested by Craig above.
Choose one among these alternative solutions which fits to your need.
I would return a viewModel for the get action
public ActionResult Index()
{
return View(new FileViewModel());
}
I would simplify the view to this
<span>#Model.FileName</span>
#using (#Html.BeginForm())
{
#Html.LabelFor(m => m.FileName)
#Html.TextBoxFor(m => m.FileName)
<input type="submit" id="SubmitBtn" value="Ok" />
}
Please also read this
MVC2 TextBoxFor value not updating after submit?

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.

Why DefaultModelBinder in MVC3 does not Bind?

I do not understand why the DefaultModelBinder in MVC3 does not map the Form Post Data to my action method. I have the following ViewModels:
public class DisplayExcelTableViewModel
{
public string UploadedFileName { get; set; }
public List<string> TableHeaders { get; set; }
public List<TableRowViewModel> TableRows { get; set; }
}
public class TableRowViewModel
{
public List<string> TableColumns { get; set; }
}
They are displayed in a (partial) View using DisplayTemplates:
#using (Html.BeginForm("SubmitExcel", "Home", FormMethod.Post))
{
<fieldset>
<table>
<tr>
<th>#Html.DisplayFor(m => m.TableHeaders)</th>//<input id="TableHeaders_0_" name="TableHeaders[0]" type="text" value="Opportunity Id" />
</tr>
<tr>
<td>#Html.DisplayFor(m => m.TableRows)</td>//<input id="TableRows_0__TableColumns_0_" name="TableRows[0].TableColumns[0]" type="text" value="1-H7PKD9" />
</tr>
</table>
<input type="submit" value="Send" />
</fieldset>
}
And the action method looks like this:
public ActionResult SubmitExcel(DisplayExcelTableViewModel excelTable)
To try whether it worked just with one TableRows I tried:
public ActionResult SubmitExcel([Bind(Prefix = "TableRows")] TableRowViewModel TableRows)
to test I also tried to put List<TableRows> and take out the Bind attribute. It does not work.
I got a runtime exception:
"System.MissingMethodException: No parameterless constructor defined for this object."
May you tell me what I am doing wrong?
Thanks Francesco
The problem is that my ViewModels DID NOT have a parameterless constructor, which is what the Default Model Binder looks for(uses .NET’s Activator.CreateInstance() method, which relies on those types having public parameterless constructors).The solutions in this case are two:
1) Add a parameterless constructor to the ViewModel and the other custom classes wrapped inside it.
2) Create a custom model binder that covers also the case of your ViewModel
Thanks
Source: Pro ASP.NET MVC2 Framework (2nd Edition)
Have you checked (for example with Firebug) whether are form values being posted to the server? I'm asking because Html.DisplayFor usually renders display elements, whereas for posting values you usually have to use Html.EditorFor.

Categories

Resources