MVC List Null on Postback - c#

My Controller populates my Model with a list with strings that appear in a DropDownList in my View. When the view is posted back to my Controller, that list is suddenly null. Why is it null, and what happened to the list of strings I created?
The list was properly populated and shows up in the View. The remainder of the form elements DO properly post back. For example, selectedName has whatever name the user clicked on. The only thing that is not posting back is nameList.
Here is the relevant part of my model,
public class MyModel
{
[Display(Name = "Selected")]
public string selectedName{ get; set; }
[Display(Name = "Names")]
public List<string> nameList{ get; set; }
}
the relevant Get and Post parts of my Controller,
public class MyController: Controller
{
[HttpGet]
public ActionResult Index()
{
List<string> nameList= getNames();
MyModel model = new MyModel()
model.nameList= nameList;
// Now, model.nameList has a bunch of stuff in it
return View(model);
}
[HttpPost]
public ActionResult Index(MyModel model)
{
if(model.nameList== null)
{
cry();
postOnStackOverflow();
}
return View(model);
}
}
and the relevant part of my View (which is encapsulated inside of a form).
<p>
#Html.LabelFor(c => c.nameList):
#Html.DropDownListFor(c => c.selectedName, new SelectList(Model.nameList), new { onchange = "this.form.submit();" })
</p>

Only the value of the drop down list is posted when you post the form. I assume that your control in question is on a form.
I am not sure why you want to always return to the view you posted from, but you need to repopulate the list:
[HttpPost]
public ActionResult Index(MyModel model)
{
List<string> names = getNames();
model.nameList = names;
return View(model);
}

That is the expected behaviour considering what you have in your view. You need to reload the namelist collection property incase you are returning model to the same view again.
[HttpPost]
public ActionResult Index(MyModel model)
{
if(ModelState.IsValid)
{
// Save and redirect
}
//reload the collection again and return the model to the view
model.nameList=getNames();
return View(model);
}

Related

Adding validation with DataAnnotations

I have a form for deleting an object from a table. I would like to put a textfield which would validate the input before pressing the Delete button.
The entity model of the objects looks like this (it has many more attributes, but I just left the important one):
public partial class card
{
public string reason { get; set; }
}
The controller method of the POST (delete) request looks like this:
// POST: /card/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
card temp_card = db.cardss.Find(id);
temp_card.deleted = true;
db.SaveChanges();
if (ModelState.IsValid)
return RedirectToAction("Index");
return View(temp_card);
}
I've read, I have to create another class and use MetaDataAnnotations for this to work, since I'm using entity models. So I wrote this:
[MetadataType(typeof(CardMetaData))]
public partial class card
{
public string reason { get; set; }
}
public class CardMetaData
{
[Required(ErrorMessage = "Write a reason for deletion.")]
public string reason { get; set; }
}
And in my Delete.aspx are the following lines:
<%= Html.ValidationSummary("Delete was unsuccessful.") %>
<div class="display-field">
<%: Html.TextBoxFor(model => model.reason) %>
<%: Html.ValidationMessageFor(model => model.reason) %>
</div>
This doesn't display a message if I press the delete button and the textfield is empty. What am I missing?
You'll need a view model with a [Required] attribute for the Reason property
public class DeleteConfirmViewModel
{
[Required]
public string Reason { get; set; }
}
Then rework your DeleteConfirmed action. You're current implementation is just not working because you first update the database and then validate if your input model is correct. Next is that you should not redirect in case of a non valid model because you will loose the ModelState containing the error (messages). A correct implementation of DeleteConfirmed will look like this:
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id, DeleteConfirmViewModel viewModel)
{
if (ModelState.IsValid)
return View(viewModel);
card temp_card = db.cardss.Find(id);
temp_card.deleted = true;
temp_card.reason = viewModel.Reason;
db.SaveChanges();
return View(temp_card);
}
In you're view you will need to show a validation message in case no reason was given
#Html.ValidationMessageFor(m => m.Reason)
So a working input field setup for the reason field in your view could look like this
#Html.LabelFor(m => m.Reason)
#Html.EditorFor(m => m.Reason)
#Html.ValidationMessageFor(m => m.Reason)
Edit
To render the DeleteConfirmed view you'll need to create a view model and pass it to the view
[HttpGet, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
return View(new DeleteConfirmViewModel());
}
[HttpPost]
public ActionResult ControllerName(ModelClassName viewModel)
{
if (!ModelState.IsValid)
return View("ViewName", viewModel);//passes validation errors back to the view
//do w.e
}

maintaning drop down selected state after post back in MVC razor?

In a MVC 4 Web I have drop-down lists with the below sample code:
#(Html.DropDownList("Condition2", new SelectList(Model.Makes, "CCultureId", "CTitle"), "All",new {#class="span3"}))
I have All as a first option in select and on button press, page shows data in it. After post back, drop downs got reset on button press, can you please guide me how to make drop down keeping its state even after page post backs (I understand in MVC4 there are no postback, I m reffering it as a round trip to server).
One way to do it is, in your controller, return the submitted value in the model. This means your dropdownlist should be hooked up to your viewmodel.
ViewModel:
public class MyViewModel
{
// more properties...
public string Make {get;set;}
// more properties
}
Controller:
[HttpPost]
public ActionResult MyAction(MyViewModel model)
{
// do postback stuff
return View(model); // model.Make is set to whatever was submitted and will be returned
}
Html:
#model Models.MyViewModel
#(Html.DropDownListFor(m => m.Make,
new SelectList(Model.Makes, "CCultureId", "CTitle", Model.Make),
"All", new {#class="span3"}))
You can use Viewbag to save the selected item, see blew:
Get Action
[HttpGet]
public ActionResult YourAction()
{
ViewBag.SelectedItem = "";
///
return View(new yourViewModel);
}
Post Action
[HttpPost]
public ActionResult YourAction(FormCollection form,YourViewModel model)
{
ViewBag.SelectedItem = form["Condition2"];
///
return View(model);
}
View
#(Html.DropDownList("Condition2", new SelectList(Model.Makes, "CCultureId",
"CTitle",ViewBag.SelectedItem), "All",new {#class="span3"}))
You can use the ? operator which works like this and use the Selected property of the selectlistitem
Console.WriteLine((2 == 2 ? "true" : "false"));
and then for example
private Entities en = new Entities();
public ActionResult Index(string selectedvalue)
{
List<SelectListItem> selectlist =
en.tblUser.Select(x => new SelectListItem { Text = x.Name, Value = x.Id,
Selected = ( x.Name == selectedvalue ? false : true) })
.ToList();
ViewBag.DropDown = selectlist;
return View();
}
then in the view u simply put this
#Html.DropDownList("DropDownName", (List<SelectListItem>)ViewBag.DropDown))
obviously its not recommended to use viewbag but instead use a model with a list property.

MVC models don't persist in a form

I declare a model with 4 string fields. 3 of which are read-only on the form:
public class HomeModel
{
[ReadOnly(true)]
[DisplayName("Service Version")]
public string ServiceVersion { get; set; }
[ReadOnly(true)]
[DisplayName("Session Id")]
public string SessionId { get; set; }
[ReadOnly(true)]
[DisplayName("Visiting from")]
public string Country { get; set; }
[DisplayName("Search")]
public string SearchString { get; set; }
}
I pass the model, after populating it, to my form:
[HttpGet]
public ActionResult Index()
{
var model = new HomeModel
{
Country = "Australia",
SearchString = "Enter a search",
ServiceVersion = "0.1",
SessionId = "76237623763726"
};
return View(model);
}
And the form is displayed as I expect:
<h2>Simple Lookup</h2>
#Html.LabelFor(m=>m.ServiceVersion): #Model.ServiceVersion<br/>
#Html.LabelFor(m=>m.SessionId): #Model.SessionId<br/>
#Html.LabelFor(m=>m.Country): #Model.Country<br/>
<p>
#using(Html.BeginForm())
{
#Html.LabelFor(m => m.SearchString)
#Html.TextBoxFor(m => m.SearchString)
<button type="submit" name="btnSearch">Search</button>
}
</p>
But, when I submit the form, and get the model back from the form, only the value of the SearchString is populated.
[HttpPost]
public ActionResult Index(HomeModel model)
{
return View(model);
}
Is it right that the other fields have been 'lost'? Does MVC not preserve the other members of the model class? And if this is expected - is there a way to re-get these? Or would I need to go back to my database, populate the model with the old values, and then use the new values from the form model?
It's possible the validity of wanting to read 'read-only' fields back from the model is questioned.. which is fair - but in the event that I find something suspect about the posted data, maybe I want to re-show the screen, and not have to re-read the data from a database again?
This is the correct behavior. Only the elements inside form will be posted to your action. Since it is posting the form so your fields should be inside the form in order to get them on your post method.
Update
Also, you cannot read particular field on your action method if you have taken that field readonly on your view. eg: displaying using #Html.LabelFor. In order to get field back on your action use #Html.HiddenFor if field is not to be edited.

NullReference error on foreach used to create radiobuttons on POST

I am teaching myself asp .net mvc3. I have a "add property" form which allows user to upload property details to the website. I have been struggling with this error for a long time now.
For simplification, lets consider that I have two tables in my database.
CustomerTypes: The database has 1 Owner, 2 Broker, 3 Commercial etc
Property: This is the table that gets populated by the form.
I use CustomerTypes (and other such tables) to create radio buttons. The user fills the form and selects a choice for "customer type". However, I get an "object reference not set to an instance of an object" error on submit. This is is because "null" is
set for Model.CustomerTypes. However, Model.CustomerTypes is only used to create radio buttons. I am not sure what is wrong. The code is below:
View:
#model Website.ViewModels.AddPropertyViewModel
<fieldset>
<legend>Property</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Property.CustomerType)
#foreach (var item in Model.CustomerTypes)
{
#Html.RadioButtonFor(model => model.Property.CustomerType, Convert.ToInt32(item.Value)) #item.Text
}
</div>
...
AddPropertyViewModel:
namespace Website.ViewModels
{
public class AddPropertyViewModel
{
public Property Property { get; set; }
...
public IEnumerable<SelectListItem> CustomerTypes { get; set; }
...
}
Controller:
public ActionResult AddProperty()
{
AddPropertyViewModel viewModel = new AddPropertyViewModel
{
...
CustomerTypes = websiterepository.GetCustomerTypeSelectList(),
...
};
return View(viewModel);
GetCustomerTypeSelectList functions is:
public IEnumerable<SelectListItem> GetCustomerTypeSelectList()
{
var customerTypes = from p in db.CustomerType
orderby p.CustomerTypeDescription
select new SelectListItem
{
Text = p.CustomerTypeDescription,
Value = SqlFunctions.StringConvert((double)p.CustomerTypeId)
};
return customerTypes;
}
The value in POST is set for Property_CustomerType correctly based on the selection
--- Added further info ---
I start the form as:
#using (Html.BeginForm("AddProperty", "Property", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
...
}
The controller is:
[HttpPost]
public ActionResult AddProperty(AddPropertyViewModel viewModel)
{
if (ModelState.IsValid)
{
//
if (viewModel.File1.ContentLength > 0)
{
var fileName = Path.GetFileName(viewModel.File1.FileName);
var path = Path.Combine(Server.MapPath("~/App_Data"), fileName);
viewModel.File1.SaveAs(path);
}
var property = viewModel.Property;
websiterepository.Add(property);
return RedirectToAction("Index", "Home");
}
return View(viewModel);
}
Here is a screenshot of error:
I have tried submitting the form commenting these radio buttons and it works.
The issue is that CustomerTypes isn't populated when your render the view after posting to the server.
If we look at the flow of actions being performed we see that
You populate the CustomerTypes collection before rendering the
inital page
You post your data back to the server but do not
preserve the CustomerTypes collection (Because there's no need to)
You render the view again but this time without populating
CustomerTypes.
Kaboom!
Populating the CustomerTypes property before you return the view for the second time should fix your problem:
[HttpPost]
public ActionResult AddProperty(AddPropertyViewModel viewModel)
{
[...]
viewModel.CustomerTypes = websiterepository.GetCustomerTypeSelectList();
return View(viewModel);
}

Model binding for a ViewModel containing multiple objects

I have a strongly typed view of type ProductListingViewModel which in turn contains a ProductViewModel. (both custom view models).
I have some form elements on my page and these are created like so:
<%: Html.DropDownListFor(m => m.ProductViewModel.CategoryId, Model.Categories)%>
which generates the HTML:
<select name="ProductViewModel.CategoryId" id="CategoryId">
With the default model binding I expected that when I post to my controller action which accepts a parameter of type ProductListingViewModel, that it'd know to populate the ProductViewModel.CategoryId with the relevant data.
The name of the select list seems to indicate that it knows there's a ProductViewModel with a CategoryId property however when I post to my controller method, the ProductViewModel is null. If I create this during construction of the ProductListingViewModel then it's no longer null but the default binder doesn't seem to be populating the properties as I expected.
Is this a case for a custom model binder or am I just missing something fundamental?
Cheers.
Let me try to summarize (correct me if I am wrong).
Model:
public class ProductListingViewModel
{
public ProductViewModel ProductViewModel { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
}
public class ProductViewModel
{
public string CategoryId { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new ProductListingViewModel
{
Categories = new SelectList(new[]
{
new { Value = "1", Text = "category 1" },
new { Value = "2", Text = "category 2" }
}, "Value", "Text")
};
return View(model);
}
[HttpPost]
public ActionResult Index(ProductListingViewModel model)
{
return View(model);
}
}
View:
<% using (Html.BeginForm()) { %>
<%: Html.DropDownListFor(m => m.ProductViewModel.CategoryId, Model.Categories)%>
<input type="submit" value="OK" />
<% } %>
Now when you submit the form you will get:
model.ProductViewModel.CategoryId = the id that was selected in the drop down list
Isn't what you are after?
It seems to me that the default binder should work in this case.
Did you try using Fiddler for checking the data sent from the client?
What exactly is the signature of the controller action?

Categories

Resources