My model is as follows:
public class testCreateModel
{
public string s1 { get; set; }
public SelectList DL { get; set; }
public testCreateModel()
{
Dictionary<string, string> items = new Dictionary<string, string>();
items.Add("1", "Item 1");
items.Add("2", "Item 2");
DL = new SelectList(items, "Key", "Value");
}
}
My initiating actions is:
public ActionResult testCreate()
{
testCreateModel model = new testCreateModel();
return View(model);
}
My Razor view (irrelevant parts deleted) is:
#model Tasks.Models.testCreateModel
#using (Html.BeginForm()) {
<fieldset>
<legend>testCreateModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.s1)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.s1)
</div>
<div class="editor-label">
Select an item:
</div>
<div class="editor-field">
#Html.DropDownList("dropdownlist", (SelectList)Model.DL)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
The post back action is:
public ActionResult testCreate(testCreateModel model, FormCollection collection)
{
if (ModelState.IsValid)
{
Console.WriteLine("SelectedValue: ",model.DL.SelectedValue);
Console.WriteLine("FormCollection:", collection["dropdownlist"]);
// update database here...
}
return View(model);
}
On post back, model.DL.SelectedValue is null. (However, the selected item can be obtained from FormCollection, but that is besides the point). The DL object is still properly populated otherwise, Immediate Window output as follows:
model.DL
{System.Web.Mvc.SelectList}
base {System.Web.Mvc.MultiSelectList}: {System.Web.Mvc.SelectList}
SelectedValue: null
model.DL.Items
Count = 2
[0]: {[1, Item 1]}
[1]: {[2, Item 2]}
model.DL.SelectedValue
null
Q1: How can I make use of the SelectedValue property instead?
Now, if in the Razor view I change the name of the Html SELECT tag to DL (ie same as the property name in the model):
#Html.DropDownList("DL", (SelectList)Model.DL)
I get an exception:
No parameterless constructor defined for this object.
Stack Trace:
[MissingMethodException: No parameterless constructor defined for this object.]
System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache) +98
System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipVisibilityChecks, Boolean skipCheckThis, Boolean fillCache) +241
System.Activator.CreateInstance(Type type, Boolean nonPublic) +69
System.Web.Mvc.DefaultModelBinder.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) +199
System.Web.Mvc.DefaultModelBinder.BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult
...
Q2: Why?
Thanks.
MVC will return just the value of the selected option in your POST, so you need a property to contain the single value that returns.
As a good advice, try setting SelectLists through ViewBag, that helps keep your ViewModels clean from data that needs to populate the form.
So your example could be solved like this:
public class testCreateModel
{
public string s1 { get; set; }
public int SelectedValue { get; set; }
}
and in your View just do this:
#Html.DropDownList("SelectedValue", (SelectList)ViewBag.DL)
prior to populating ViewBag.DL in your GET action.
As for your Q2, the default ModelBinder requires that all types to bind to have a default constructor (so that the ModelBinder can create them)
An answer has been selected, but look at how I did it. Below is code how I normally do it when populating a drop down. It is very simplistic, I suggest you use it as a base to build your drop downs.
At the top of my view I specify my view model:
#model MyProject.ViewModels.MyViewModel
On my view I have a drop down list that displays all the banks that a user can select from:
<table>
<tr>
<td><b>Bank:</b></td>
<td>
#Html.DropDownListFor(
x => x.BankId,
new SelectList(Model.Banks, "Id", "Name", Model.BankId),
"-- Select --"
)
#Html.ValidationMessageFor(x => x.BankId)
</td>
</tr>
</table>
I always have a view model for a view, I never pass a domain object directly to the view. In this case my view model will contain a list of banks that will be populated from the database:
public class MyViewModel
{
// Other properties
public int BankId { get; set; }
public IEnumerable<Bank> Banks { get; set; }
}
My bank domain model:
public class Bank
{
public int Id { get; set; }
public string Name { get; set; }
}
Then in my action method I create an instance of my view model and populate the banks list from the database. Once this is done then I return the view model to the view:
public ActionResult MyActionMethod()
{
MyViewModel viewModel = new ViewModel
{
// Database call to get all the banks
// GetAll returns a list of Bank objects
Banks = bankService.GetAll()
};
return View(viewModel);
}
[HttpPost]
public ActionResult MyActionMethod(MyViewModel viewModel)
{
// If you have selected an item then BankId would have a value in it
}
I hope this helps.
Related
I'm implementing asp.net core 3.1. I have three radio buttons in my razor view and with the following code, I want to send the selected radio button value to Index action in controller in order to show its related data. My problem is after choosing a radio button and after that select the button. it send null value to the Index method.
Here my radio button code in razor
#model CSD.ChartObjects
<form method="post">
#foreach (var year in Model.Years)
{
<input type="radio" asp-for="Year" value="#year" />#year<br />
}
<input type="submit" asp-action="Index" />
</form>
Here is my model object that is read in razor
public class ChartObjects
{
public List<ChartModel> Percent { get; set; }
public List<ChartModel> Time { get; set; }
public List<ChartModel> Avg { get; set; }
public List<ChartModel> Total { get; set; }
[BindProperty]
public string Year { get; set; }
public string[] Years = new[] { "1398", "1399", "1400" };
}
And here is the body of my HomeController:
[HttpGet]
public IActionResult Index()
{
return (BuildIndexModel("1399"));
}
[HttpPost]
public IActionResult Index([FromForm] string currentYear)
{
return (BuildIndexModel(currentYear));
}
public IActionResult BuildIndexModel([FromForm] string currentYear)
{
...
}
The problem you're having is because of this:
[HttpPost]
public IActionResult Index([FromForm] string currentYear)
On your form, the value you want to bind is called Year, but in your POST action, you're specifying a parameter called currentYear. MVC doesn't know that you want to bind the value of Year to currentYear. Model binding is all based on the convention of property names on your model being the same as the data being sent from a form.
So all you need to do is change your POST method to this:
[HttpPost]
public IActionResult Index(string year)
Notice how I've also removed the [FromForm] attribute. You don't need to specify that here in order for it to bind properly. I'm guessing you added it while trying to figure out why it wasn't working. For the same reason, you also don't need the [BindProperty] attribute on your viewmodel - it's the name of the property that is important.
I'm new to MVC 3 and I have a question regarding the correct approach.
Imagine I have a model:
public class MyCustomModel
{
[Required]
public string UserName { get; set; }
[Required]
public DateTime? Birthdate { get; set; }
[Required]
public string City {get;set;} //To partial view
[Required]
public string Street {get;set;} //To partial view
}
And here I have a view
#Html.TextBoxFor(m => m.UserName)
#Html.TextBoxFor(m => m.BirthDate)
#Html.Action("LocationGroup", "Home") //In this should the city and street be rendered
My Partial View will have somethign like that:
#Html.TextBoxFor(m => m.City)
#Html.TextBoxFor(m => m.Street)
And this the action in the controller:
[ChildActionOnly]
public ActionResult LocationGroup()
{
MyCustomModel model = new MyCustomModel (); //Should i really instantiate a new instace of the model??? and pass it to the partial view
return PartialView("_TempView", model);
}
Basically my general view will have all the field with texboxex, but now in my partial view i also would like to have few of those propeties from my model be rendered correctly and after submiting the form should be available in the same model as all other properties.
So my question, in the action which send the partial view back, should i really instantiate a new instace of the model? But then the data will be split between 2 instances of the model no?
How to arrange that, how can i then ass the data to the general views model from partial view?
i didnt get your question but you can annotate the ActionResults with HttpGet and HttpPost having same names (but different signatures, because they are methods after all) like
[HttpGet]
[ChildActionOnly]
public ActionResult LocationGroup()
{
Model model = new Model();
return PartialView("_TempView", model);
}
in the view you must be doing something like
#model YOURMODELNAME
#using(Html.BeginForm("LocationGroup","Controller",FormMethod.POST)){
#Html.TextBoxFor(x=>x.UserName)
#Html.TextBoxFor(x=>x.Birthdate )
<input type="submit" value="submit" />
}
now define a post type ActionResult
[HttpPost]
[ChildActionOnly]
public ActionResult LocationGroup(YOUR_MODEL_TYPE model)
{
if(ModelState.IsValid){
//do something
}
}
the default model binder will look into the HttpContext for the match between the posted value names and the properties of your model and bind the value automatically
I am trying to create an Item that has an ItemType coming from another table. I am unable to get back the actual Type object from the Create page. This is the code I have tried:
Models:
public class ItemType {
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Item> Item{ get; set; }
}
public class Item {
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ItemType ItemType { get; set; }
}
In the ItemController, this is my create code:
public ActionResult Create() {
var itemTypeRepo = new ItemTypeRepository(context);
ViewBag.ItemTypes = new SelectList(itemTypeRepo.GetAll(), "ID", "Name");
return View();
}
[HttpPost]
public ActionResult Create(Item item) {
if (ModelState.IsValid) {
context.Items.Add(item);
context.SaveChanges();
return RedirectToAction("Index");
}
return View(item);
}
In my Create.cshtml view I have tried:
<div class="editor-field">
#Html.DropDownList("ItemType", String.Empty)
#Html.ValidationMessageFor(model => model.ItemType)
</div>
This returns no value at all and throws an error "The value 'X' is invalid." Where X is the ID of the ItemType I selected.
And
<div class="editor-field">
#Html.DropDownListFor(x => x.ItemType.Id, (SelectList)ViewBag.ItemType)
#Html.ValidationMessageFor(model => model.ItemType)
</div>
This creates a stub ItemType object with the correct ID but does not insert it into the database since the object is not fully loaded. If I look at ModelState object, I find that there is an error that the Name field is missing from ItemType object.
I also attempted to solve the problem using the second .cshtml code and adding this code:
public ActionResult Create(Item item) {
item.ItemType = context.ItemTypes.Find(item.ItemType.Id);
if (ModelState.IsValid)
This does not change the value of ModelState.IsValid from false even through it should.
What do I need to do to get this to work?
You should add a property ItemTypeId to your Item entity so that it acts as a foreign key.
public class Item
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public int ItemTypeId { get; set; }
[ForeignKey("ItemTypeId")]
public virtual ItemType ItemType { get; set; }
}
You can then use that property for the dropdownlist:
<div class="editor-field">
#Html.DropDownListFor(x => x.ItemTypeId, (SelectList)ViewBag.ItemType)
#Html.ValidationMessageFor(model => model.ItemType)
</div>
I'm having some trouble getting my ViewModel to return a non-null object in the base model property in my controller's Create action postback. I currently have another page that is doing the exact same operations on another model that has almost the same properties and that form is working perfectly, so I feel like I'm missing something basic, though I can't place what is wrong.
Here's my ViewModel with my Base class:
public class SystemFailureActionViewModel
{
/// <summary>
/// View model class for adding and modifying SystemFailureActions
/// </summary>
public SystemFailureAction action { get; set; }
//properties
public int TypeID { get; set; }
public string TypeDescription { get; set; }
public bool Assigned { get; set; }
public SystemFailureActionViewModel() { }
public SystemFailureActionViewModel(SystemFailureAction action)
{
this.action = action;
}
//Collections for views
public IEnumerable<SelectListItem> EditSystemFailiureTypesList { get { return ModelListProvider.FilteredSystemFailureTypeList; } }
public IEnumerable<SelectListItem> DetailsSystemFailiureTypesList { get { return ModelListProvider.FullSystemFailureTypeList; } }
}
[MetadataType(typeof(SystemFailureActionMetadata))]
public partial class SystemFailureAction
{
private class SystemFailureActionMetadata
{
/// <summary>
/// ID for this failure action
/// </summary>
public int ID { get; set; }
/// <summary>
/// Action Description
/// </summary>
[Required]
[StringLength(200)]
[DisplayName("Action Description")]
public string Description { get; set; }
}
}
Here's my Controller Add and Add Postback methods:
public ActionResult Add()
{
SystemFailureAction action = new SystemFailureAction();
action.Description = "";
populateSystemFailureActionData(action);
return PartialView("Form", new SystemFailureActionViewModel(action));
}
[HttpPost]
public ActionResult Add(SystemFailureActionViewModel viewModel, string[] selectedTypes, FormCollection collection)
{
SystemFailureAction newAction = viewModel.action;
if (!ModelState.IsValid)
{
populateSystemFailureActionData(newAction);
return PartialView("Form", new SystemFailureActionViewModel(newAction));
}
try
{
//Insert the new failure action type
context.SystemFailureActions.InsertOnSubmit(newAction);
context.SubmitChanges();
//Insert the failure type mappings
updateSystemFailureAssociationData(newAction, selectedTypes);
context.SubmitChanges();
//Return the new data
populateSystemFailureActionData(newAction);
return PartialView("Done", new SystemFailureActionViewModel(newAction));
}
catch (Exception ex)
{
ModelState.AddModelError("", ex.Message);
populateSystemFailureActionData(newAction);
return PartialView("Form", new SystemFailureActionViewModel(newAction));
}
}
And finally here is my form, it is being loaded into a Jquery dialog and the postback is being done via Ajax.
#using (Ajax.BeginForm(new AjaxOptions
{
HttpMethod = "Post",
UpdateTargetId = "formDialog",
InsertionMode = InsertionMode.Replace,
OnSuccess = "onDialogDone()"
}))
{
#Html.ValidationSummary(true)
<div>
<div class="editor-label">
#Html.LabelFor(model => model.action.Description)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.action.Description)
</div>
</div>
<div class="categoryFieldSet">
<fieldset>
<legend>Mechanical System Failure Categories</legend>
<div class="editor-field">
<table>
<tr>
#{
int count = 0;
List<ManageMAT.ViewModels.SystemFailureActionViewModel> types = ViewBag.Types;
foreach (var type in types)
{
if (count++ % 3 == 0)
{
#: </tr> <tr>
}
#: <td>
<input type="checkbox"
id="selectedTypes + #type.TypeID"
name="selectedTypes"
value="#type.TypeID"
#(Html.Raw(type.Assigned ? "checked=\"checked\"" : "")) />
<label for="selectedTypes + #type.TypeID">#type.TypeDescription</label>
#:</td>
}
#:</tr>
}
</table>
</div>
</fieldset>
</div>
}
If you're wondering what the ViewBag.Types logic is in the form it is related to this question I asked earlier.
Edit:
I checked the ModelState error and the exception I'm getting is
"The parameter conversion from type 'System.String' to type Models.SystemFailureAction' failed because no type converter can convert between these types."
I also removed the logic that adds the Failure types and I'm still receiving the same issue. So it appears the problem is coming from mapping the Viewmodel.action.Description to the action.
The problem is the following property on your SystemFailureActionViewModel:
public SystemFailureAction action { get; set; }
In ASP.NET MVC action and controller are kinda reserved words. They are part of every route. And if you have a property called this way it conflicts with the string value which is pointing to the action name and which obviously cannot be bound to a complex SystemFailureAction type.
So to fix the problem simply rename this property on your view model:
public SystemFailureAction FailureAction { get; set; }
I have a number of child ViewModel classes which inherit from an base ViewModel class.
I pass my child ViewModel into my View, which then passes itself into a partial view.
The main view takes the child type, but the partial view takes the Parent type.
Everything displays correctly when I manually populate the properties.
However, when I Submit the form, my controller action only has properties for the Child class - none of the base class properties are completed?
e.g.
public abstract class BaseDetails
{
public string Name { get; set; }
public BaseDetails()
{ }
public BaseDetails(string name)
{
Name = name;
}
}
public class LocalDetails:BaseDetails
{
public int Visits { get; set; }
public LocalDetails()
{ }
public LocalDetails(int visits, string name)
:base(name)
{
Visits = visits;
}
}
With the View as simple as:
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.Visits)
<br />
#Html.Partial("Name", Model)
<input id="Submit1" type="submit" value="submit" />
}
The partial view has a single textbox on it.
In IE: ViewSource shows the form tag is around both tetxboxes.
However, when I submit to the controller method:
[HttpPost]
public ActionResult EditLocal (LocalDetails model)
model.Visits is correctly populated, but model.Name is null?
Any ideas?
I added parameterless constructors to the classes because I got an exception upon submission if I didn't.
Unable to reproduce. Works fine for me. Notice that I am not using any constructors with my view models because the model binder wouldn't be able to call them and that all the properties must to have public getters and setters.
Model:
public abstract class BaseDetails
{
public string Name { get; set; }
}
public class LocalDetails : BaseDetails
{
public int Visits { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new LocalDetails());
}
[HttpPost]
public ActionResult Index(LocalDetails model)
{
return View(model);
}
}
View (~/Views/Home/Index.cshtml):
#model LocalDetails
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.Visits)
<br />
#Html.Partial("Name", Model)
<input type="submit" value="submit" />
}
Partial (~/Views/Home/Name.cshtml):
#model BaseDetails
#Html.TextBoxFor(x => x.Name)
When I submit the form inside the POST Index action both model properties Name and Visits are correctly bound with values from the form.