ASP.NET MVC : Changing model's properties on postback - asp.net-mvc

I've been playing with ASP.NET MVC and ran into something I can't figure out.
Suppose I have an object like this :
public class TestObject
{
public string Name { get; set; }
public int Age { get; set; }
}
And a view page (Create.aspx) like this :
<form action="/Create" method="post">
<p>
<%=Html.TextBox("Name") %>
</p>
<p>
<%=Html.TextBox("Age")%>
</p>
</form>
And on my controller I have these actions :
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Create()
{
return View(new TestObject { Name = "DefaultName", Age = 10 } );
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(TestObject o)
{
o.Name = "ChangedNameToSomethingElse";
o.Age = 15;
return View(o);
}
The Html.TextBox() method always genereates the textboxes with the default values, even after the postback, where the object is passed back with different properties on its values. Now, granted, I can't think of a real world example why I'd want to do such a thing but I still don't understand why I always end up having textboxes populated with the model's values that were set on the Create action with the AcceptVerbs(HttpVerbs.Get) attribute.
Note : I've tried Html.TextBox("Name", Model.Name) but the result is still the same. And I verified that the Create action with AcceptVerbs(HttpVerbs.Post) actually runs, by passing a value via ViewData to the View.
Also, the udated value is displayed when I output the value with <%=Model.Name %> but again, not on the textbox.
Is there something obvious I'm missing, or is there a reasoning behind this behaviour?

If you bind the result of a post request through the declaration of the method or by UpdateModel or TryUpdateModel to an object such as TestObject, a property called ModelState will get filled in with these values. The HTML helpers such as Textbox will always bind to modelstate over an explicitly passed model object.

I know this was answered a long time ago, but this is more targeted solution that works for me.
[HttpPost]
public ActionResult Create(TestObject o) {
ModelState.Remove("Name");
o.Name = "ChangedNameToSomethingElse";
ModelState.Remove("Age");
o.Age = 15;
return View(o);
}
Hope this helps someone out there.

Try this one:- (hope it will work for you)
[HttpPost]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(TestObject o) {
ModelState.Clear();
o.Name = "ChangedNameToSomethingElse";
o.Age = 15;
return View(o);
}

Related

Object reference not set when passing TextAreaFor to controller

The app is designed to allow the user to enter a an IP address for a local machine and and it will then return the HDD information for that machine. It starts out with a default value already in the TextAreaFor box and performs the query for that value. This part works with no problem. But when a user tries to enter in their own value and hit the Refresh button, it keeps coming up with the error Object reference not set to an instance of an object.
I'm not sure why this is happening. It seems to me that clicking the button submits a POST action, which should kick off the second method in the controller. The current model is then passed to the controller with the values in the TextAreaFor attached and the mainCode() method is run on the new values.
Edit: According to What is a NullReferenceException, and how do I fix it? I am pretty sure that I am returning an empty model from my controller. I just don't see how. The form field should be sending the controller everything contained in TextAreaFor so the model should not be empty.
Edit2: I did some testing and the model is getting returned alright, but the values from TextAreaFor are not. When the mainCode() tries to do some logic to startDrives.startingDrives, it can't because that variable is empty for some reason.
Model:
namespace RelengAdmin.Models
{
public class DriveInfo
{
public class DriveHolder
{
public string startingDrives {get; set;}
}
public DriveHolder startDrives = new DriveHolder();
public void mainCode()
{
/****Code to return the HDD size omitted****/
}
}
}
View:
#using (Html.BeginForm())
{
<input type="submit" value="Refresh" />
#Html.TextAreaFor(model => model.startDrives.startingDrives, new {#class = "HDDTextBox"})
}
Controller:
namespace RelengAdmin.Controllers
{
public class HDDCheckerController : Controller
{
[HttpGet]
public ActionResult Index()
{
DriveInfo myDrive = new DriveInfo();
myDrive.startDrives.startingDrives = "148.136.148.53"
myDrive.mainCode();
return View(myDrive);
}
[HttpPost]
public ActionResult Index(DriveInfo model)
{
model.mainCode();
return View(model);
}
}
}
The issue is that your model's startDrives property is not actually declared as a property with getters and setters, so the model binder won't bind to it. I was able to duplicate the issue locally, and solve it by declaring the startDrives as a property and initializing it in the constructor.
public class DriveInfo
{
public class DriveHolder
{
public string startingDrives { get; set; }
}
public DriveHolder startDrives { get; set; }
public DriveInfo()
{
startDrives = new DriveHolder();
}
public void mainCode()
{
/****Code to return the HDD size omitted****/
}
}
Your question is a bit unclear of where the model is actually null.. but I would assume that when you hit your button, it goes to the correct action, but there is nothing in model because you haven't passed any specific values..
so try this:
CSHTML
#using (Html.BeginForm())
{
<input type="submit" value="Refresh" />
#Html.TextArea("startingDrive", "148.136.148.53", new {#class = "HDDTextBox"})
}
Controller
[HttpPost]
public ActionResult Index(string startingDrive)
{
DriveInfo searchThisDrive = new DriveInfo();
searchThisDrive.startDrives.startingDrives = startingDrive;
searchThisDrive.mainCode();
return View(searchThisDrive);
}
Let me know if this helps!

Asp.Net MVC3 - Complex class not being passed as null on [HttpPost] method

its me... yet again!
Ive got these class,
public class PrankTargetArgumentViewModel
{
public PrankTarget Target { get; set; }
public PrankDefinition Prank { get; set; }
public List<PrankArgument> Arguments { get; set; }
}
public class PrankArgument
{
public string Name { get; set; }
public string Value { get; set; }
}
and what I'm doing is - if this current ParkDefinition needs arguments them im doing an ActionRedirect on the save to another Action which should handle the gathering of the Arguments
My Action result is like this..
public ActionResult PrankArguments()
{
PrankInstance currentInstance = SessionContext.CurrentPrankInstance;
if (currentInstance == null)
throw new ArgumentNullException("currentInstance");
PrankTargetArgumentViewModel model = new PrankTargetArgumentViewModel();
model.Prank = currentInstance.Prank;
model.Target = currentInstance.Target;
string[] args = model.Prank.Arguments.Split('|');
model.Arguments = new List<PrankArgument>();
foreach (string s in args)
{
model.Arguments.Add(new PrankArgument { Name = s, Value = s });
}
return View(model);
}
my http post method is just an empty method with the parameter of PrankTargetArgumentViewModel
[HttpPost]
public ActionResult PrankArguments(PrankTargetArgumentViewModel model)
{
return View();
}
My HTML is like this..
#using (Html.BeginForm())
{
#Html.EditorFor(x => Model)
<p>
<input type="submit" value="Create" />
</p>
}
So my problem is this, on the PrankArguments(PrankTargetArgumentViewModel model) post back action, the model param is always null.. I've filled the object with values on the load so I guessed they would be there on the post back with the new arguments that I added.
so the flow goes like this.
Create Prank
If prank needs arguments then load ActionResult PrankArguments()
Add extra arguments to an already poplulated object.
save, Call ActionResult PrankArguments(PrankTargetArgumentViewModel model)
-- this is where the problem is, the model parameter is passed back as null.
Ive had this problem quite a few times and always just given up but im not going to let that happen this time!
any help would be great! cheers, Ste!
Ps. If you need anymore of my code just let me know.
EDIT - Removed view bag debug properties!
I think if I understand you correctly if your view is strongly typed to PrankTargetArgumentViewModel then all you have to do to retrieve the values is:
[HttpPost]
public ActionResult PrankArguments()
{
var pta = new PrankTargetArgumentViewModel();
TryUpdateModel(pta);
}
After reviewing my own code - I noticed that I didn't need the entire PrankTargetArgumentViewModel and a simple List of Arguments would have been fine.
I alterd my PrankArguments view to take an IEnumerable and used;
#using (Html.BeginForm())
{
#Html.EditorForModel()
<p>
<input type="submit" value="Finish" />
</p>
}
then had my post back method signature like this
[HttpPost]
public ActionResult PrankArguments(IEnumerable<PrankArgument> arguments)
which worked exactly how I wanted.
Thanks for all the suggestions guys.

DefaultModelBinder Problem with nested levels + other binders

I have what I would think is a somewhat normal situation where I need to bind form posts to an "order" model. This model has a few levels of information to it:
Order.Billing.FirstName
Order.Billing.Address.City
Order.Billing.Address.Country
Using the DefaultModelBinder, if I POST a form to an action that takes this Order model as the param, the following fields JustWork(TM):
<%=Html.TextBox("Billing.FirstName")%>
<%=Html.TextBox("Billing.Address.City")%>
This field does not:
<%=Html.TextBox("Billing.Address.Country")%>
The wrinkle I have is with the country property. In our case, Address.Country returns a Country class instance (ISO2/3/Name/Code logic). It is not a string. Not surprise that it doesn't work by default.
My first thought was to create a CountryModelBinder (inherit DefaultModelBinder) and ModelBinders.Binders.Add it to the type of Country. When I do that, CountryModelBinder never gets called in the scenerio above.
My second thought was to create an AddressModelBinder (inherit DefaultModelBinder) and bind it to our Address type. While that does get called, the SetProperty call for "Country" has an empty value, even though the form has posted a field called "Billing.Address.Country".
After some tinkering, it appears that the model binding behavior only calls CreateModel when the model is the top level class the action wants, and all other binders have their BindPropery/SetProperty called for child properties.
In other words, if I create model binders for Order, OrderAddress(Billing), Address, and Country. For the action that takes an order, only OrderModelBinder.CreateModel is called. ORderAddress and Address.BindProperty/SetProperty are called for some things, and sometimes SetProperty value argument is empty when it was clearly posted in a name that matches the other field property mappings.
It's easy enough to just add code to OrderModelBinder to pull Billing.Address.Country out of Request.Form. But I have multiple models that use Address and having all of them do that seems broken.
What am I missing here? Is there a way to have the CountryModelBinder actually get called in this case? I would think that the CountryModelBinder should get called when Billing.Address.Country is mapped to the Country property of the Address binder.
I've tried doing what you've done here, appearntly on MVC3 it does indeed work if I provide a model binder for that type.
This is just a proof of concept to show that it DOES WORK, and shouldn't be seen as even close to production level code:
Models:
public class SimpleModel
{
public string Value { get; set; }
public int Other { get; set; }
}
public class ComplexModel
{
public SimpleModel Complexity {get;set;}
public string StrVal { get; set; }
}
some binder:
public class MBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if ( bindingContext.ModelType == typeof(SimpleModel))
{
var simpleModel= new SimpleModel();
simpleModel.Other = 1;
simpleModel.Value = controllerContext.HttpContext.Request.Form["Complexity"];
return cm;
}
return null;
}
}
in global asax:
ModelBinders.Binders.Add(typeof (SimpleModel), new MBinder());
code in View:
#model ComplexModel
#using ( Html.BeginForm() )
{
<fieldset>
#Html.LabelFor(x => x.Complexity)
#Html.TextBoxFor(x => x.Complexity)
</fieldset>
<fieldset>
#Html.LabelFor(x => x.StrVal)
<br />
#Html.EditorFor(x => x.StrVal)
</fieldset>
<input type="submit" />
}
Controller:
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(ComplexModel model)
{
return RedirectToAction("Index");
}
BTW in MVC 3 a better option would be to use the IModelBinderProvider interface, but I just wanted to show something that would work.

Model binding and display trimmed string property

My strongly typed View inherits from a "Person" object that is created with Linq to SQL. In my "Edit" View, i have to display of course old values:
<%= Html.TextBox("FirstName") %>
"FirstName" is NCHAR, so it need to be trimmed. So i ended up with:
<%= Html.TextBox("FirstName", Model.FirstName.Trim()) %>
and this works. But when form is submitted (after POST) and some errors occur, i need to show it again:
[AcceptVerbsAttribute(HttpVerbs.Post), Authorize(Roles = "office"), HandleError]
public ActionResult Edit(Models.Person person)
{
if (!(_personService.ValidatePerson(person))) // Persona non valida
{ return View(person); }
}
If for some reason the user left the textbox "FirstName" blank, the resulting property Person.FirstName become null and Model.FirstName.Trim() throws an Exception (Object reference not set to an instance of an object).
Any way to modify the bind and have all string trimmed by default? Or any ideas to how fix this?
Update: seems confirmed to be a MVC 2 behaviour.. still looking for a good way to handle this. Actually using an extension method:
public static string TrimOrDefault(this string value)
{
return value != null ? value.Trim() : string.Empty;
}
Something is odd here.
Model.FirstName shouldn't be null; the model binder is smart enough to set an empty input field ( textbox ) to "". Make sure your property names match up between your model and the textboxes your using.
Which version of MVC are you using? 1 or 2? I'm running a MVC 1 version in VS 2008 and the only way I can get FirstName to be null is by not including it in the form at all.
I could see if you initial GET Edit view threw this error and you had FirstName set to nullable in your dbml and database but since it is a Post this doesn't make sense to me right now. ;)
Update:
I've confirmed this:
With an empty form:
VS 2008 - Mvc 1 - FirstName = ""
VS 2010 - Mvc 2 - FirstName = null
Uh oh... Thats going to break a lot of code...
The Code:
View ( same for both ):
<% using (Html.BeginForm()) {%>
<fieldset>
<legend>Fields</legend>
<p>
<label for="FirstName">FirstName:</label>
<%= Html.TextBox("FirstName", Model.FirstName) %>
<%= Html.ValidationMessage("FirstName", "*") %>
</p>
<p>
<label for="LastName">LastName:</label>
<%= Html.TextBox("LastName", Model.LastName) %>
<%= Html.ValidationMessage("LastName", "*") %>
</p>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
VS 2010 - Mvc 2
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About()
{
return View();
}
public ActionResult AddPerson()
{
var person = new Person();
return View(person);
}
[HttpPost]
public ActionResult AddPerson(Person person)
{
return View(person);
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
VS 2008 - Mvc 1
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About()
{
return View();
}
public ActionResult AddPerson()
{
var person = new Person();
return View(person);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult AddPerson( Person person )
{
return View(person);
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
No clean fix atm. The model binder is actually setting those properties to null. Set First and Last to "" in the constructor and exclude those properties from binding: [Bind(Exclude="FirstName, LastName")] they stay "".
Is this documented someplace?
Try a custom modelbinder.
See this : ASP.NET MVC: Best way to trim strings after data entry. Should I create a custom model binder?
In Models.Person have the default value be String.Empty which should elminate the null error at least.
If you want to have it automated, then you can add that into your dbml file. There is a designer.cs file attached and each field has a getter and setter. you can add code in there if you wish.
you could also create a partial class based on your table and handle the trim within that. it means that if you make a change to the dbml file then you don't lose your changes.
if you're not using a dbml file then let us know.
EDIT
If you have a class called person in your dbml file then this will already be declared as partial.
create another class, in the same project and do something like the following;
public partial class Person
{
public string NewName
{
get { return this._name.Trim(); }
}
}
So from then on in you can use .NewName instead of name and it will come back trimmed. You can also add code in there to ensure it's not null, not red, not whatever and return the appropriate value.
I'm not aware of an extension method that does this, if someone else does then please let me know.
I create a simple HTML helper for fields that COULD be null.
public static string TrimOrDefault(string value)
{
return (value == null ? "" : value.Trim());
}
And then in your code you can use
<%= Html.TextBox("FirstName", Helpers.TrimOrDefault(Model.FirstName)) %>
It's re-usable for future nullable fields and reads easily.

Error trying to populate a drop down list during a "GET" create action

I am trying to understand something a bit better with being new to C#, .NET 3.5 and MVC.
I am running through the MVC NerdDinner example and if you look at the ViewModel here: http://nerddinnerbook.s3.amazonaws.com/Part6.htm#highlighter_662935
You can see the Country list and how it gets populated, this seems to work fine but I tried to do a similar thing below using LINQ and I am having problems, with the SelectList approach even though it inherits from the IEnumerable interface.
I have got a task table with a foreign key to a status table. The below code gives me a NullReferenceException when I do a GET on a create action. I can see that an anonymous task object would not have a status set.. so I probably need to check for it, but I dont understand how this is not done for the NerdDinner example??
public class TaskViewModel {
// Properties
public Task Task { get; private set; }
public SelectList Status { get; private set; }
// Constructor
public TaskViewModel(Task task) {
TaskRepository taskRepo = new TaskRepository();
Task = task;
Status = new SelectList(taskRepo.GetStatus(), Task.Status.description);
}
}
//
// GET: /Tasks/Create
public ActionResult Create()
{
Task task = new Task();
return View(new TaskViewModel(task));
}
//Code from TaskRepository
private TaskManagerDataContext db = new TaskManagerDataContext();
public IQueryable<Status> GetStatus() {
return from status in db.Status
orderby status.description
select status;
}
I did another approach using LINQ for the type dropdown and the population of the drop down works but I am yet to test if it selects the correct value once a post is made and the details view is returned. I am also wondering whether this should some how be moved into my repository rather than have a class in my controller doing this sort of thing??
Here is the code:
//In TaskViewModel Class
public IEnumerable<SelectListItem> Types { get; private set; }
//In TaskViewModel constructor
IList<NPType> types = taskRepo.GetTypes().ToList();
Types =
from type in types
select new SelectListItem {
Selected = (type.typeId == task.typeId),
Text = type.description,
Value = type.typeId.ToString()
};
//The TaskForm partial View that is used for the Create action of the TaskController
<p>
<label for="type">type:</label>
<%= Html.DropDownList("Type", Model.Types)%>
<%= Html.ValidationMessage("type", "*") %>
</p>
<p>
<label for="status">status:</label>
<%= Html.DropDownList("Status", Model.Status)%>
<%= Html.ValidationMessage("status", "*") %>
</p>
and the TaskForm view inherits System.Web.Mvc.ViewUserControl
What's in your task constructor? What is the value of .typeId on a newly created task? Is it a null reference?
For the view model sent to your Create view, you shouldn't be trying to set the selected list item unless your task constructor (or other initialization code) sets default values for those properties. If task.typeId is null, then your code that is building the select list will get an error.
I understand that I will get a null value for the type or status if I dont add a value to the newly created task. What I dont understand and which I didnt make clear is the below.
You can see the view model has a Countries property, and its selected value is set to Dinner.Country.. now Dinner.Country is not being set in the create action.. so how come this does not give a null exception?
//viewmodel code
public DinnerFormViewModel(Dinner dinner) {
Dinner = dinner;
Countries = new SelectList(PhoneValidator.Countries, Dinner.Country);
}
//controller code
public ActionResult Create() {
Dinner dinner = new Dinner() {
EventDate = DateTime.Now.AddDays(7)
};
return View(new DinnerFormViewModel(dinner));
}
//view code
<p>
<label for="Country">Country:</label>
<%= Html.DropDownList("Country", Model.Countries) %>
<%= Html.ValidationMessage("Country", "*") %>
</p>
My attempt at trying to understand this better.
//controller code creating a select list in the viewmodel class.
//taskRepo.GetStatus() returns an IQueryable<Status>
Status = new SelectList(taskRepo.GetStatus(), Task.Status);
//MVC Framework SelectList class constructor and ToEnumerable method
public SelectList(IEnumerable items, string dataValueField, string dataTextField, object selectedValue)
: base(items, dataValueField, dataTextField, ToEnumerable(selectedValue)) {
SelectedValue = selectedValue;
}
private static IEnumerable ToEnumerable(object selectedValue) {
return (selectedValue != null) ? new object[] { selectedValue } : null;
}
I can see that SelectList uses its base class of MultiSelectList and that constructor is here:
public MultiSelectList(IEnumerable items, string dataValueField, string dataTextField, IEnumerable selectedValues) {
if (items == null) {
throw new ArgumentNullException("items");
}
Items = items;
DataValueField = dataValueField;
DataTextField = dataTextField;
SelectedValues = selectedValues;
}
When I run this project the html is given as:
<select id="Status" name="Status"><option>NPTaskManager.Models.Status</option>
<option>NPTaskManager.Models.Status</option>
<option>NPTaskManager.Models.Status</option>
<option>NPTaskManager.Models.Status</option>
</select>
Which is to be expected.
If I change the controller code to:
Status = new SelectList(taskRepo.GetStatus(), Task.Status.statusId.ToString(), Task.Status.description);
Then I get a NullReferenceException. Since this is not an ArgumentNullException It seems to me that the root of the exception is not the first SelectList argument. What I am trying to understand is how this all occurs?
Is it because Task.Status needs to be added to Task in the create action of the controller?
I will change this code to use the LINQ approach that I used for the task above, all I am trying to achieve now is some understanding of what is going on.

Resources