List can only store one item in MVC? - c#

Im struggling to pass data from view into a list in the controller.
I can add 1 item to the list, but when the controller goes back to the view to recieve another input from the user, the previous item in the list is replaced with the new one. The last item just isnt there anymore, so the list always has just 1 item in the count. Its almost like it just resets everytime the user input a new string. As if it cant store more than one string at a time.
I want to store multiple user inputs in the AddTagVM.StringList, so that I can eventually retrieve and print them in a view.
This is my model:
public class AddTagVM
{
public Post Post { get; set; }
public List<Tag> TagList { get; set; }
public List<string> StringList { get; set; } = new List<string>();
public string TagName { get; set; }
}
This is the controller:
public ActionResult AddTag()
{
AddTagVM tagg = new AddTagVM();
return View(tagg);
}
[HttpPost]
public ActionResult AddTag(AddTagVM tagg)
{
//tagg.Tag[tagg.Add].Name = tagg.TagName;
tagg.StringList.Add(tagg.TagName);
return View(tagg);
}
And this is the view:
#model BlogNiKRaMu.Models.AddTagVM
#{
ViewBag.Title = "AddTag";
}
<h2>AddTag</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Post</h4>
<hr />
#Html.EditorFor(model => model.TagName)
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>

You can remove = new List<string>(); from the VM first as see if it works as expected.
Else try to do it like this.
[HttpPost]
public ActionResult AddTag(AddTagVM tagg)
{
var tag = tagg.TagName; // <---Extract the value to a variable first.
tagg.StringList.Add(tag);
return View(tagg);
}

When you post back the form, your AddTagVM object will only have TagName input not the list value as you lose the list. Meaning, when post the form, the request should contain all the values you need to retain in the object i.e the StringList should also be posted along with string.
So the work around you can try is, you can add HidderFor to the StringList. This way when the form post the object will have value for TagName and StringList as well.
#Html.HiddenFor(model => model.StringList)
#for(int i= 0; i<StringList.count(); i++)
{
#Html.HiddenFor(model=>model.StringList[i])
}

Related

Post values of select box to controller

I have a model, MyModel, with a list property and an accompanying string used to store the values in the database.
Simplifying a bit to increase readability.
public class MyModel
{
public int ID { get; set; }
public List<int> Numbers { get; set; }
[Column, ScaffoldColumn(false)]
public string NumbersStore { get { /*parses Numbers*/ } set { /*splits the value and sets Numbers accordingly*/ } }
}
I'm using basic CRUD, based off the scaffolding.
In my Create/Edit views, I have manually written a select multiple. Not using helpers was probably a bad idea. I have no problem retrieving these values in the Index, Details, and Delete views, but I cannot figure out how to actually bind this data to my model when creating/editing.
Just from some blind Googling, I've tried:
- Added a list to the MyModelController.Create parameters list
- Attempted to use Request.Form["SelectNameAttribute"]
CSHTML:
#using (Html.BeginForm())
{
<div class="form-group">
#Html.LabelFor(model => model.Numbers, htmlAttributes: new { #class = "control-label col-md-2" })
<select class="select2 col-md-10 form-control" multiple="multiple" id="Numbers" name="Numbers">
<!-- Ultimately enumerated with a loop that goes through the database, probably butchering MVC conventions. Don't think that's relevant to the question. -->
<option value="1">One</option>
<option value="2">Two</option>
<!-- Etc. -->
</select>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
}
Controller:
public ActionResult Create([Bind(Include = "ID,NumbersStore")] MyModel myModel) // Changing NumbersStore to Numbers does nothing
{
//myModel.NumbersStore = Request.Form["Numbers"].ToString();
if (ModelState.IsValid)
{
db.MyModels.Add(myModel);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(myModel);
}
Just recapping the solution from the comments in the question.
The [Bind(Include = "")] needs to include the the properties from the form being posted.
In this case the NumbersStore needs to be Numbers to match the property in the HTML.
[HttpPost]
public ActionResult Create([Bind(Include = "ID,Numbers")] MyModel myModel)
{
// do something with numbers
db.MyModels.Add(myModel);
db.SaveChanges();
return RedirectToAction("Index");
}
You can pass values to the controller just my simply assigning a parameter with the same name as the html tag:
You can give your select a name (notice the brackets for a list of items)
<select class="select2 col-md-10 form-control" multiple="multiple" id="Numbers" name="Numbers[]">
<option value="1">One</option>
<option value="2">Two</option>
</select>
Then in controller use parameter int[] Numbers to access the posted values
[HttpPost]
public ActionResult Create([Bind(Include = "ID,NumbersStore")] MyModel myModel, int[] Numbers)
{
// do something with numbers
db.MyModels.Add(myModel);
db.SaveChanges();
return RedirectToAction("Index");
}
Lloyd's answer got me close.
I look like a fool here. The answer is as simple as naming the select and binding that same name to the controller action -- something I could have sworn I'd done, but I think I bound the store rather than the actual list.
I'm not sure if the name attribute has to be the same as the name of the model's property, but I did it to be safe (and for clarity).
So the correct code is:
MyModel.cs
public class MyModel
{
public int ID { get; set; }
public List<int> Numbers { get; set; } // This is the list we're changing
[Column, ScaffoldColumn(false)]
public string NumbersStore { get { /*parses Numbers*/ } set { /*splits the value and sets Numbers accordingly*/ } }
}
MyModelView.cshtml
#using (Html.BeginForm())
{
<div class="form-group">
#Html.LabelFor(model => model.Numbers, htmlAttributes: new { #class = "control-label col-md-2" })
<select class="select2 col-md-10 form-control" multiple="multiple" id="Numbers" name="Numbers"> // Notice name attribute
<option value="1">One</option>
<option value="2">Two</option>
<!-- Etc. -->
</select>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
}
MyModelController.cs .Create
public ActionResult Create([Bind(Include = "ID,Numbers")] MyModel myModel) // Notice name attribute in Bind
{
if (ModelState.IsValid)
{
db.MyModels.Add(myModel);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(myModel);
}
Note:
- Create does not need an array as a parameter (unless you need to make further changes)
- The name HTML attribute does not need to end with [] -- as described here by Sergey, that's a PHP convention that has no effect on ASP.NET
- As far as I know, the name attribute does need to be the same as the property that is eventually being mutated

List is NULL while POST from View

I use the below Action to allow the user to see a preview of Excel import
[HttpGet]
[Authorize]
public ActionResult ImportVerify()
{
string temp_sessionname = "ImportedData_" + User.Identity.Name;
List<ProjectImportModel> view_model = (List<ProjectImportModel>)TempData[temp_sessionname];
return View(view_model);
}
[HttpPost]
[Authorize]
public ActionResult ImportVerify(List<ProjectImportModel> model)
{
return View(model);
}
And on View i am using a table to show the List of imported data from excel and ask user to confirm the action of import
My view is like this
<h2>Import Verify</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
//table with all details and a submit button in the end
<div class="form-group">
<div class="col-md-10" style="text-align:center;">
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
</div>
}
And model is
public class ProjectImportModel
{
public string Page { get; set; }
public string Author { get; set; }
public string Translator { get; set; }
public string Holder { get; set; }
public string Title { get; set; }
public string TrTitle { get; set; }
//and similar 20 more properties of string type
}
But on POST the list is null
Is any way to get the list back at POST event. My intension is just to allow the preview to user
Or do i need to refill List from TempData # post as well?
In order to post a collection back you need to index the properties, if they're readonly you can just use HiddenFor with a model.
If you want the user to edit them, change them to TextBoxFor's instead or the control that you data requires.
#model List<ProjectImportModel>
<h2>Import Verify</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
//table with all details and a submit button in the end
<div class="form-group">
<div class="col-md-10" style="text-align:center;">
#for(var i = 0 ; i < Model.Count; i++)
{
#Html.HiddenFor(m => m[i].Foo)
#Model[i].Foo <br/>
}
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
</div>
}
I just used a dummy property of Foo without seeing your model.
Obviously you would want to display the data too.
Model Binding To A List
I don't see the snippet code that set your TempData. So I intend that you set it in another Action and then redirect to the ImportVerify Action
TempData keep the information for the time of an HTTP Request. This mean only from one page to another. It's mean that after redirect to your ImportVerify Get Action the data is expired. If you want to keep the data you can try the following ways:
Create a hidden List in your page
Use Session instead of TempData
There are some requirements to post a list back to a controller. Specifically, indexes must be 0-based and unbroken (example, you can bind a list with indexes [0], [1], [2] but not one with [0], [1], [3] or [1], [2], [3].
Alternatively, you could write a custom model binder to parse the request body the way you like.
More on this here: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
EDIT:
Here is an example on how to do it:
Given the following model
public class ExampleModel
{
public int Field1 {get; set;}
public string Field2 {get; set;}
}
And the following actions
[HttpGet]
[Authorize]
public ActionResult ImportVerify()
{
List<ExampleModel> model = //populate the list somehow
return View(model);
}
[HttpPost]
[Authorize]
public ActionResult ImportVerify(List<ExampleModel> model)
{
//do something
}
The example view "ImportVerify.cshtml" would be:
#model List<ExampleModel>
#using(Html.BeginForm())
{
for(var i = 0; i < Model.Count; i++)
{
<div>
#Html.HiddenFor(m => m[i].Field1);
#Html.HiddenFor(m => m[i].Field2);
<p>
Value #i : Field1 = #m[i].Field1 , Field2 = #m[i].Field2
</p>
</div>
}
<input type="submit" value="Send"/>
}
Also, I would revise your logic a bit, to avoid the use of TempData (generally bad practice) and to allow strongly typed views.

ASP.NET MVC one particular view model property becoming null on returning to view

In ASP.NET MVC I have a view that's a form, and I want to be able to save the form and then it returns back to the same page showing the data you entered after saving the data to the database. I'm sure I'm just doing something stupid (this is pretty new to me), but there are some properties that I want to persist and I am setting them on the view model before I return, and I have #Html.HiddenFor inside my view's form. My confusion is that of these items are retained, and some aren't. So I have the following inside my FormController (methods and names have been simplified for brevity):
public ActionResult Index(int? p, int? c)
{
FormViewModel model = new FormViewModel();
model.p = p;
model.c = c;
model.dateStarted = DateTime.Now;
return View(model);
}
[HttpPost]
public ActionResult Index(FormViewModel m)
{
Form form;
bool shouldUpdate = false;
if (m.formID != null) // m.formID is always null, but m.p, c, dateStarted aren't
{
shouldUpdate = true;
form = getFormnWithId((int)m.formID); //gets from database
}
else
{
form = new Form(m);
}
if (shouldUpdate)
{
editForm(form); //edit existing entry
}
else {
addForm(form); //add to database
}
m.formID = form.Id; // formn.Id is valid because the form has been updated with its Id after being added to the database
m.p = form.p;
m.c = form.c;
return View(m);
}
Inside my view (cshtml) file I have #Html.HiddenFor(model=>model.formID) as well as for other properties I want to persist but aren't being set in the form directly.
The formID however, is not persisting, while the other items (represented by c and p and dateStarted) are fine. If I remove the HiddenFor for those other fields, then they don't work. I click save each time, and formID is null in the post, but it's definitely set after the form has been added to the database and the value of the formID is definitely getting sent to the view. I just don't understand why it comes back null but the other properties don't.
Here's what the model looks like:
public class FormViewModel
{
public Nullable<int> formID {get; set;}
public Nullable<int> c { get; set; }
public Nullable<int> p { get; set; }
public System.DateTime dateStarted { get; set; }
//+ other form properties
}
View:
...
<label for="submit-form" class="btn btn-default">Save</label>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal col-md-12">
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
<!-- various form fields -->
</div>
#Html.HiddenFor(model => model.dateStarted)
#Html.DisplayFor(model => model.dateStarted)<br /> <!-- just to see it while testing-->
#Html.HiddenFor(model => model.c)
#Html.DisplayFor(model => model.c)<br />
#Html.HiddenFor(model => model.p)
#Html.DisplayFor(model => model.p)<br />
#Html.HiddenFor(model => model.formID)
#Html.DisplayFor(model => model.formID)
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" name="Command" class="btn btn-default hidden" id="submit-form" />
</div>
</div>
</div>
}
Now that I see you are setting the Form.Id in the POST request, your issue is that you are not following the PRG (Post, Redirect, Get) pattern. You are returning the same view from your POST method without any type of redirect. As a result, the model binder is holding on to the previous value of Form.Id, which was null. The reason that the model binder holds on to previous values is mainly for validation purposes (if the ModelState has an error, you can return the view, the properties remain as the user entered them along with the ModelState errors collection)
To fix this, you either need to redirect to another action or issue ModelState.Clear() in your code before you return the view.
m.formID = form.Id; // form.Id is valid because the form has been
//updated with its Id after being added to the database
m.p = form.p;
m.c = form.c;
ModelState.Clear();
return View(m);

How do I keep the clicked state of a checkbox in asp.net

I'm really having problems with keeping the state of my checkbox in my mvc4 application. I'm trying to send its value down to my controller logic, and refresh a list in my model based on the given value, before I send the model back up to the view with the new values. Given that my checkbox is a "show disabled elements in list" type function, I need it to be able to switch on and off. I've seen so many different solutions to this, but I can't seem to get them to work :(
Here's a part of my view:
#model MyProject.Models.HomeViewModel
<div class="row-fluid">
<div class="span12">
<div class="k-block">
<form action="~/Home/Index" name="refreshForm" method="POST">
<p>Include disabled units: #Html.CheckBoxFor(m => m.Refresh)</p>
<input type="submit" class="k-button" value="Refresh" />
#* KendoUI Grid code *#
</div>
</div>
HomeViewModel:
public class HomeViewModel
{
public List<UnitService.UnitType> UnitTypes { get; set; }
public bool Refresh { get; set; }
}
The HomeViewController will need some refactoring, but that will be a new task
[HttpPost]
public ActionResult Index(FormCollection formCollection, HomeViewModel model)
{
bool showDisabled = model.Refresh;
FilteredList = new List<UnitType>();
Model = new HomeViewModel();
var client = new UnitServiceClient();
var listOfUnitsFromService = client.GetListOfUnits(showDisabled);
if (!showDisabled)
{
FilteredList = listOfUnitsFromService.Where(unit => !unit.Disabled).ToList();
Model.UnitTypes = FilteredList;
return View(Model);
}
FilteredList = listOfUnitsFromService.ToList();
Model.UnitTypes = FilteredList;
return View(Model);
}
You return your Model to your view, so your Model properties will be populated, but your checkbox value is not part of your model! The solution is to do away with the FormCollection entirely and add the checkbox to your view model:
public class HomeViewModel
{
... // HomeViewModel's current properties go here
public bool Refresh { get; set; }
}
In your view:
#Html.CheckBoxFor(m => m.Refresh)
In your controller:
[HttpPost]
public ActionResult Index(HomeViewModel model)
{
/* Some logic here about model.Refresh */
return View(model);
}
As an aside, I can't see any reason why you'd want to add this value to the session as you do now (unless there's something that isn't evident in the code you've posted.

MVC4 Complex Type Model is null after post

This is my model
public class AdministrationModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public bool IsApproved { get; set; }
}
This is my controller
public ActionResult GetTabContent(string id)
{
switch (id)
{
case "tab3":
model = GetAllUsersInfo();
viewName = "Administration";
break;
}
return View(viewName);
}
private List<AdministrationModel> GetAllUsersInfo()
{
List<AdministrationModel> userList = new List<AdministrationModel>();
foreach (MembershipUser user in Membership.GetAllUsers())
{
UserProfile userProfile = UserProfile.GetUserProfile(user.UserName);
userList.Add(new AdministrationModel { EmailAddress = user.Email, IsApproved = user.IsApproved, FirstName = userProfile.FirstName, LastName = userProfile.LastName });
}
return userList;
}
This is my View
#model List<AdminContainerModel>
#using (Html.BeginForm("Administration", "Account"))
{
<fieldset>
<div>
#foreach (AdministrationModel AM in Model)
{
<div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.FirstName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.LastName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.EmailAddress)</div>
<div class="colPartial"><input type="checkbox" checked="#AM.IsApproved"/> </div>
<div class="clear"></div>
</div>
}
</div>
<input type="submit" value="Update Account" />
</fieldset>
}
When the user clicks the Update Account button it goes to the controller
[HttpPost]
public ActionResult Administration(List<AdministrationModel> model)
{
return View();
}
inside this method, model is always null. however the View that renders everything is perfect and shows what I want it to show. What am I doing wrong?
When using collections, in order to correctly process them so they are model-bound on post without any additional leg work, you need to make sure they are indexed correctly, you can do this by using a for loop, something like:
#for (int i = 0; i < Model.Count; i++)
{
#Html.HiddenFor(m => m[i].FirstName)
#Html.HiddenFor(m => m[i].LastName)
#Html.HiddenFor(m => m[i].EmailAddress)
<div>
<div class="colFull">#Html.DisplayFor(m => m[i].FirstName)</div>
<div class="colFull">#Html.DisplayFor(m => m[i].LastName)</div>
<div class="colFull">#Html.DisplayFor(m => m[i].EmailAddress)</div>
<div class="colPartial">#Html.CheckBoxFor(m => m[i].IsApproved)</div>
<div class="clear"></div>
</div>
}
That should model bind without any other code :)
Edit: Sorry I forgot, displayFors by default don't put the correct properties on for model binding, added hiddenFors for the other fields that don't have an editorFor
Edit2: Based on your other question in the comment, if it was public facing and you didn't want them to change any of the hidden for values using the dev tools, try the following:
Ok so you don't want them to change the hiddenFors, that's fine, but you will need some sort of ID so you know which client is which when the data is posted, I suggest that instead of having these in the above code:
#Html.HiddenFor(m => m[i].FirstName)
#Html.HiddenFor(m => m[i].LastName)
#Html.HiddenFor(m => m[i].EmailAddress)
Replace them with:
#Html.HiddenFor(m => m[i].ClientId)
That way you're not posting back the firstname, lastname or email address, just a reference to the actual client that is ticked, unticked.
To answer your other question in the comment about keeping track of the original values, in your controller method you can just go and get the original values from the database, then here's how you can detect which ones are different, something like:
[HttpPost]
public ActionResult Administration(List<AdministrationModel> model)
{
var originalMatches = GetAllUsersInfo();
var differences = (from o in originalMatches
join c in model on o.ClientId equals c.ClientId
where c.IsApproved != o.IsApproved).ToList()
return View();
}
Your #model directive is incorrect, it should be the fully qualified type name.
In this case:
#model TheNamespace.AdministrationModel
After your updated question.
Try using:
#Html.EditorFor(Model)
This will call a specialized Editor Template for your list.
http://blogs.msdn.com/b/nunos/archive/2010/02/08/quick-tips-about-asp-net-mvc-editor-templates.aspx
If you want to return list items with the model on submit you will need to use a for loop, not a foreach
Matty's answer will work.
If you want to avoid having to specify indexes (which wouldn't work if you had an ICollection) you can define a child template as Xander has explained.
The benefit is you still get all your strong typed intellisense in the AdminContainerModel View and MVC hooks the items back in to your List on post back for you out of the box.
Here's an example:
Main Editor template (as Xander said):
#model IEnumerable<AdminContainerModel>
#using (Html.BeginForm("Administration", "Account"))
{
<fieldset>
<div>
#Html.EditorForModel()
</div?
</fieldset>
}
AdminContainerModel Editor template(This is called for each item in your List because you've called #Html.EditorForModel:
#model AdminContainerModel
<div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.FirstName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.LastName)</div>
<div class="colFull">#Html.DisplayFor(modelItem => AM.EmailAddress)</div>
<div class="colPartial"><input type="checkbox" checked="#AM.IsApproved"/>
</div>
<div class="clear"></div>

Categories