ASP.NET MVC 3 Save and edit from the same form - asp.net-mvc

We have a form and depending upon the circumstances we want to switch between an add operation and an update operation from the same form. Below is a cut down version of our form.
Effectively the "Order number" textbox is disabled and can never be edited in this form. Now, the scenario is a bit like this:
The first time the user lands on this form, the "Order number" text box is blank.
The user enters a customer name and submits the form.
At this point in the controller action, we get the max value of order number in the database and increment the order number by 1 . We then add that new record in the database.
If that operation is successful, we update the current form and the "Order Number" textbox should now be updated with the order number created in the previous step AND also what should happen is that we are now in Edit mode.
Say the user then updates the "Customer name" and submits the form, the record in the database should be updated in this instance.
Now for some code:
The View:
<%: Html.TextBox("OrderNumber", Model.OrderNumber == 0 ? "" : Model.OrderNumber.ToString(), new { #disabled = "true" })%>
The controller:
public ActionResult Index()
{
var customerOrderModel = new CustomerOrderModel();
return View(customerOrderModel);
}
public ActionResult Add(CustomerOrderModel customerOrderModel, FormCollection values)
{
// We write the logic for either the add or update.
return this.View("Index", customerOrderModel);
}
I have removed the code from the Add action because from putting breakpoints we know that the "// We write the logic for either the add or update." is not the problem.
Now where we are having trouble is this. We can add the new entry in the table following which the "Order Number" field gets updated and is displayed correctly. However, after we change the customer name and try to update, the customerOrderModel passed into the "Add" action shows that the order number being passed is 0(which is our default in the system and which is used to determine if we are performing an add or update operation).
So the question is why is our textbox getting updated, which would seem to indicate that our model is getting updated, but then when we try to submit, the correct model doesn't get passed in? Moreover, why is it that the Index action doesn't get hit after the "Add" action is completed? What do we have to do to get things to work the way we want them to?

Model
namespace Demo.Models
{
public class Order
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
}
public class OrderDb:DbContext
{
public DbSet<Order> Orders { get; set; }
}
}
View
#model Demo.Models.Order
<form action="/" method="post">
<table>
<tr>
<td>#Html.LabelFor(m=>m.OrderId)</td>
<td>
#Html.EditorFor(m => m.OrderId)
</td>
</tr>
<tr>
<td>
#Html.LabelFor(m=>m.CustomerName)
</td>
<td>
#Html.EditorFor(m=>m.CustomerName)
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" name="submit" value="submit" />
</td>
</tr>
</table>
</form>
Action
public ActionResult Index(Order o)
{
if (o.CustomerName != null)
{
using (OrderDb db = new OrderDb())
{
db.Entry(o).State = o.OrderId == 0 ? EntityState.Added : EntityState.Modified;
db.SaveChanges();
ModelState.Clear();
}
}
return View(o);
}

This is because HtmlHelpers look to ModelState for values first and then uses the values you explicitly use.
So when you add the entity you get ["Id"]=0 inside your model state.
So solve you have to clear your ModelState with .Clear() after a successful add.

Related

Model Not Submitting / Binding Correctly on POST

Scenario:
I have a table with rows that are populated from a ViewModel. There are checkboxes for each row that allow the user to check 1 or more of the rows and then choose from actions in a dropdown menu to make edits to properties on the selected rows.
Everything works fine to this point, and I can get the ViewModel to pass correctly and then use it and all it's properties in a POST Action method. I could make the changes based on the option the user picked.
However, since some of the options in the dropdown would make fairly substantial and irreversible changes, I am calling a new View with a GET and populating a new table with just the selected rows, and asking the user to confirm they want to make the changes. Everything is still good up to this point. The new View populates as expected with only the rows that were selected in the previous View.
Problem:
After the user confirms their intent, an Action method is called with POST. The ViewModel that correctly populated the current View is making its way into the controller correctly. I get the ViewModel, but not with the same properties as the one that populated the View.
ViewModel
public class ProjectIndexViewModel
{
public List<ProjectDetailsViewModel> Projects { get; set; }
public string FlagFormEditProjects { get; set; }
public string FlagFormNewProjectStatus { get; set; }
}
The List<ProjectDetailsViewModel> Projects is what is used to populate the rows in the table, and Projects are what are not binding correctly in the POST Action methods in the controller.
Initial View where the checkboxes are selected. Note the example of one of the javascript functions that is called when one of the dropdown options is selected, which is what submits the form.
#using (Html.BeginForm("EditProjectsTable", "Project", FormMethod.Get, new { name = "formEditProjects", id = "formEditProjects" }))
{
#Html.HiddenFor(item => item.FlagFormEditProjects)
#Html.HiddenFor(item => item.FlagFormNewProjectStatus)
....
<table>
<thead>
....
</thead>
<tbody>
#for (int i = 0; i < Model.Projects.Count; i++)
{
<tr>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectNumber)</td>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectWorkType)</td>
.... // more display properties
<td>
#Html.CheckBoxFor(x => x.Projects[i].Selected, new { #class = "big-checkbox" })
#Html.HiddenFor(x => x.Projects[i].ProjectModelId)
</td>
</tr>
}
</tbody>
</table>
}
function submitFormRemoveProjects() {
$("#FlagFormEditProjects").attr({
"value": "RemoveProjects"
});
$('#formEditProjects').submit();
}
Action method that returns the "confirmation" View (works fine)
[HttpGet]
[Authorize(Roles = "Sys Admin, Account Admin, User")]
public async Task<ActionResult> EditProjectsTable([Bind(Include = "Projects,FlagFormEditProjects,FlagformNewProjectStatus")]ProjectIndexViewModel projectIndexViewModel)
{
// Repopulate the Projects collection of ProjectIndexViewModel to
// include only those that have been selected
return View(projectIndexViewModel);
}
View that is returned from Action method above (works fine) Note that the Action method that gets called is set dynamically with the actionName variable in the Html.BeginForm call.
#using (Html.BeginForm(actionName, "Project", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.FlagFormNewProjectStatus)
....
<table>
<thead>
....
</thead>
<tbody>
#for (int i = 0; i < Model.Projects.Count; i++)
{
<tr>
<td>#Html.HiddenFor(x => x.Projects[i].ProjectModelId)</td>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectNumber)</td>
<td>#Html.DisplayFor(x => x.Projects[i].ProjectWorkType)</td>
.... // more display properties
</tr>
}
</tbody>
</table>
<input type="submit" value="Delete Permanently" />
}
An example of one of the Controller Action methods that is called from this View, and that does not have the same Project that was in the View. Somehow, it has the same number of Projects that were originally selected, but if only one was selected, it has the Project with the lowest Model Id. I'm not sure how else to describe what's happening. But in summary, the correct ViewModel is not making it's way into the POST method example shown below.
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Sys Admin, Account Admin")]
public async Task<ActionResult> DeleteConfirmedMultipleProjects([Bind(Include = "Projects")] ProjectIndexViewModel projectIndexViewModel)
{
if (ModelState.IsValid)
{
// Remove Projects from db and save changes
return RedirectToAction("../Project/Index");
}
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Please help!
The issue is that when you submit to the EditProjectsTable() method from the first view, the values of all form controls are added to ModelState.
Repopulating your collection of ProjectDetailsViewModel does not update ModelState, and when you return the view, the DisplayFor() methods will display the correct values because DisplayFor() uses the values of the model, however your
#Html.HiddenFor(x => x.Projects[i].ProjectModelId)
will use the values from ModelState, as do all the HtmlHelper methods that generate form controls (except PasswordFor()).
One way to solve this is to call ModelState.Clear() before you return the view in the EditProjectsTable() method. The HiddenFor() method will now use the value of the model because there is no ModelState value.
[HttpGet]
[Authorize(Roles = "Sys Admin, Account Admin, User")]
public async Task<ActionResult> EditProjectsTable(ProjectIndexViewModel projectIndexViewModel)
{
// Repopulate the Projects collection of ProjectIndexViewModel to
// include only those that have been selected
ModelState.Clear(); // add this
return View(projectIndexViewModel);
}
For a explanation of why this is the default behavior, refer the second part of this answer.
Side note: Your using a view model, so there is no point including a [Bind] attribute in your methods.
I think your problems comed from this part:
#Html.CheckBoxFor(x => x.Projects[i].Selected, new { #class = "big-checkbox" })
#Html.HiddenFor(x => x.Projects[i].ProjectModelId)
I had this error before and what I did is adding a Boolean property to ProjectDetailsViewModel like IsSelected.
then you should have :
#Html.CheckBoxFor(x => x.Projects[i].IsSelected, new { #class = "big-checkbox" })
Then on method you should add:
foreach (var project in ProjectIndexViewModel.Projects )
{
if (project.IsSelected==true)
"put your logic here"
}

Confusion about binding a radio button to a model in MVC

I'm new to MVC. I'm trying to build a small website for people to pick winners in NFL games. I am confused on how to translate what a user picks in the view to the data in my database. I am using MVC 5 and Entity Framework 6.
I want the view to list each game, displaying the home team and the away team. Next to each team there is a radio button. The user selects the radio button next to each selection, and then clicks a Submit button. For each game, I want to read which of the two radio buttons has been selected, and then write the name of the related team to a database table for Picks. The database is already built and I can't change it.
So I have a model for Picks (id, week_number, game1, game2, etc). I have a model for Schedule, which is all of the NFL games this season (id, week_number, game_number, home, away). I have the basic controller from scaffolding it out, and I am trying to create a ViewModel to combine the two models for my what my view needs.
What I do not know how to do is add a radio button so that it is tied to the team name it is next to, so that when I submit it will be determined if that button is checked, and if so, to write that team name to the database. Am I supposed to make 32 boolean properties for each radio button and then if it is checked look up the team name in a list of all the games? It seems like this should be much easier, but I am stumped with the whole MVC structure.
Also, is this supposed to happen in the controller, and not the view model? How do I access which radio button is selected?
UPDATE
I followed the suggestions here as best I could, and I am closer. But the problem now is that all of the radio buttons on the page are in one radio button group - that is, only one button can be selected. I want to have each row in the table be a group, so that ultimately there will be 16 buttons selected. Here are the models and view I am using:
public class GameViewModel
{
public string AwayTeam { get; set; }
public string HomeTeam { get; set; }
public string SelectedTeam { get; set; }
public GameViewModel()
{
AwayTeam = string.Empty;
HomeTeam = string.Empty;
SelectedTeam = string.Empty;
}
public GameViewModel(string away, string home)
{
AwayTeam = away;
HomeTeam = home;
SelectedTeam = string.Empty;
}
}
public class WeeklyPicksViewModel
{
private NFLEntities db = new NFLEntities();
public int MNFscore { get; set; }
public List<GameViewModel> WeeklySchedule { get; set; }
public int WeekNumber { get; set; }
public WeeklyPicksViewModel(List<schedule> weeklySchedule, int userid)
{
List<GameViewModel> week = new List<GameViewModel>();
foreach (var game in weeklySchedule)
{
GameViewModel g = new GameViewModel(game.away, game.home);
week.Add(g);
}
WeeklySchedule = week;
}
}
#model FootballPickEm.ViewModels.WeeklyPicksViewModel
<h2>Picks Page</h2>
<hr />
<div>
<table>
#foreach (var game in Model.WeeklySchedule) {
<tr>
<td>
#Html.RadioButtonFor(m => game.SelectedTeam, game.AwayTeam)
</td>
<td>
<img src="~/images/#(game.AwayTeam.ToLower()).gif" />
</td>
<td>
#Html.DisplayFor(m => game.AwayTeam)
</td>
<td>
AT
</td>
<td>
#Html.RadioButtonFor(m => game.SelectedTeam, game.HomeTeam)
</td>
<td>
<img src="~/images/#(game.HomeTeam.ToLower()).gif" />
</td>
<td>
#Html.DisplayFor(m => game.HomeTeam)
</td>
</tr>
}
</table>
</div>
Good news. You don't need to create 32 bools. You can just create one string (or enum or whatever else) that represents the team. If you call the property team, you can generate radio buttons like this.
<label><input type="radio" name="team" value="piggers">The Piggers</label>
<label><input type="radio" name="team" value="dogcatchers">The Dogcatchers</label>
Then when you submit the form, your team property will contain a value like "piggers".

Modified view-model value in post not showing up in DropDownListFor

I'm trying to implement a button to adding and removing a row using static HTML. I saw this question that seems to be what I want, but I found that the example doesn't work for drop-down lists. None of the drop-down lists' options ever are marked as selected. If I don't clear the model state, all of the old values are kept. How can I keep my changes to the view-model?
// Controller action
[HttpPost]
public virtual ActionResult DoSomething(DoSomethingViewModel viewModel)
{
if (viewModel != null)
{
if (viewModel.ButtonPressed != null)
{
if (viewModel.ButtonPressed.Trim() == "Cancel")
{
return Redirect(ApplicationUtilities.CancelRequestUrl);
}
else if (viewModel.ButtonPressed.Trim() == "AddRow")
{
ModelState.Clear();
// This only covers non-JavaScript users.
// One for the newest one.
viewModel.FieldOneValues.Add(String.Empty);
viewModel.FieldTwoValues.Add(String.Empty);
viewModel.FieldThreeValues.Add(null);
return View(viewModel);
}
else if (viewModel.ButtonPressed.Trim().StartsWith("Remove"))
{
ModelState.Clear();
String[] split = viewModel.ButtonPressed.Split('-');
if (split.Length == 2)
{
Int32 indexToRemove;
Regex regex = new Regex(#"\[([0-9]+)\]");
Match match = regex.Match(split[1]);
if (match.Success && Int32.TryParse(match.Groups[1].Value, out indexToRemove))
{
viewModel.FieldOneValues.RemoveAt(indexToRemove);
viewModel.FieldTwoValues.RemoveAt(indexToRemove);
viewModel.FieldThreeValues.RemoveAt(indexToRemove);
}
}
return View(viewModel);
}
}
}
if (ModelState.IsValid)
{
return WhateverIsDoneOnSuccess(viewModel);
}
else
{
return View(viewModel);
}
}
// View Model
public class DoSomethingViewModel
{
public DoSomethingViewModel()
{
this.FieldOneValues = new List<String>();
this.FieldTwoValues = new List<String>();
this.FieldThreeValues = new List<Int32?>();
}
public virtual IList<String> FieldOneValues { get; set; }
public virtual IList<String> FieldTwoValues { get; set; }
public virtual IList<Int32?> FieldThreeValues { get; set; }
public virtual String ButtonPressed { get; set; }
}
<!-- Spark View -->
<tr each="var fieldOneValue in Model.FieldOneValues">
<td headers="FieldOneTh">${Html.TextAreaFor(m => m.FieldOneValues[fieldOneValueIndex])}</td>
<td headers="FieldTwoTh">${Html.TextAreaFor(m => m.FieldTwoValues[fieldOneValueIndex])}</td>
<td headers="FieldThreeTh">
${Html.TextBoxFor(m => m.fieldOneValueIndex], new { disabled="disabled", #readonly="readonly" })}
${Html.DropDownListFor(
m => m.FieldThreeValues[fieldOneValueIndex]
, ApplicationUtilities.FieldThreeSelectListItems
, " "
)}
</td>
<td headers="AddRemoveTh">
<button name="${Html.NameFor(m => m.ButtonPressed)}" class="Remove" type="submit" value="Remove-[${fieldOneValueIndex}]">Remove</button>
<button if="fieldOneValueIsLast" name="${Html.NameFor(m => m.ButtonPressed)}" class="Add" type="submit" value="AddRow">Add</button>
</td>
</tr>
<!-- HTML Output -->
<tr>
<td headers="FieldOneTh"><textarea cols="20" id="FieldOneValues_0_" name="FieldOneValues[0]" rows="2">
</textarea></td>
<td headers="FieldTwoTh"><textarea cols="20" id="FieldTwoValues_0_" name="FieldTwoValues[0]" rows="2">
</textarea></td>
<td headers="FieldThreeTh">
<input data-val="true" data-val-number="The field Nullable`1 must be a number." disabled="disabled" id="FieldThreeValues_0_" name="FieldThreeValues[0]" readonly="readonly" type="text" value="0" />
<select id="FieldThreeValues_0_" name="FieldThreeValues[0]"><option value=""> </option>
<option value="0">Option 1</option>
<option value="1">Option 2</option>
<option value="2">Option 3option>
</select>
</td>
<td headers="AddRemoveTh">
<button name="ButtonPressed" class="Remove" type="submit" value="Remove-[0]">Remove</button>
<button name="ButtonPressed" class="Add" type="submit" value="AddRow">Add</button>
</td>
</tr>
Plus I'm curious; I think there should be a way to do this.
There is, but you have to handle the post correctly. It's situations like these why PRG (Post-Redirect-Get) is recommended. When you click something like a remove button for a particular item, it's not appropriate to save all the other fields and do whatever else would happen when the whole form is actually submitted. All the user indicated was that they wanted to remove this one item.
Therefore, when you get the post, you remove that item from the database or wherever it's persisted and then you redirect back to the original form if that's what you want. The redirect process updates the page state so that the item is now gone and the rest of the form can then be edited without carrying around stale data. What you're trying to do is remove the item, but then just return the view directly which still has the posted item in the data backing it. That's where your problem is.
I think you went down this path because you're trying to maintain any edits the user made to other areas of the form, but that's simply not going to be possible. However, you do have some options:
Don't actually have a button that removes the item right this minute. Instead, provide a checkbox or something that indicates the item should be deleted when the user posts the entire form. Then you can save the all the form data, remove the indicated items, and redirect afterwards like you should.
Use local storage to save the user's edits on the client-side, and then read them back from local storage after the page loads again, following the redirect. However, this requires JS.
Use AJAX to submit the request to remove the item, and then remove the row from the DOM. However, this requires JS.
Also, remember that it's entirely possible to progressively enhance your form. So, you can implement #1 and #3, and then if JS isn't available, #1 still serves as a fallback.

How to capture selected row ID - MVC Table

I have table layout in MVC (see below code), on each table row I have a submit button. Each Submit button post to same controller method 'TableSample'. How to capture the selected row id and post it?
public class TableSample
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public int Property3 { get; set; }
public List<Things> Things;
}
#Html.TextBoxFor(m => m.Property1)
#Html.TextBoxFor(m => m.Property2)
#Html.TextBoxFor(m => m.Property3)
<table>
<tbody>
#foreach (var thing in Model.Things)
{
<tr>
<td>#thing.ID</td>
<td>#thing.Name</td>
<td><input type="submit" value="Select" name="Command" /></td>
</tr>
}
</tbody>
</table>
[HttpPost]
public ActionResult TableSample(TableSample sample, string Command)
{
if (Command == "Select")
{
//How to capture selected row ID?
}
if (Command == "Other")
{
}
}
Use javascript to catch the submit button click and place the row id in a hidden field, that way it will be submitted with the rest of the fields.
If the row id will not be part of your model you can simply add a parameter to the action method with the same name as the hidden field.
Let me know if you need more details. I have done basically the same thing in one of my mvc applications.
Basically 3 steps:
1) Add the hidden input. We'll just use straight HTML and not helpers since the field will not be part of the model. Place this somewhere in the form:
<input type="hidden" id="rowId" name="rowId" />
2) Modify the action method signature to include the new parameter (I assume it is an integer but you can change the type accordingly if it is not):
public ActionResult TableSample(TableSample sample, string Command, int rowId)
3) Add the javascript to catch the submit button click and place the row id in the hidden field. I prefer jQuery and I assume you have access to it since it's pretty standard for MVC 4:
$(function () {
$('input[name="command"]').click(function () {
// because there is a command button on each row it is important to
// retrieve the id that is in the same row as the button
$('#rowId').val($(this).parents('tr:first').children('td:first').html());
});
});
It would be a little easy if you noted what you mean by rowID, bacause it is absent in you code. But for all I understand you mean id from first of the row.
In Controller:
[HttpPost]
public ActionResult TableSample(TableSample sample, string Command, int rowid)
{
if (Command == "Select")
{
rowid
}
if (Command == "Other")
{
}
}
In View:
<script>
$('input[name=Command]').click(function(){
var rowID = $(this).closest('tr').find(".rowid").val()
$post('/Home/TableSample?rowid='+rowID+ '&Command=Select')
});
</script>
<table>
<tbody>
#foreach (var thing in Model.Things)
{
<tr>
<td class="rowid">#thing.ID</td>
<td>#thing.Name</td>
<td><input type="submit" value="Select" name="Command" /></td>
</tr>
}
</tbody>
</table>

ASP.NET MVC FormCollection TextArea

I have a textarea that represents a description field. The descriptions have commas so when trying to split the field's descriptions the data is not parsed correctly. How can I get each row's description correctly.
var DescList = FormValues["Item.Description"].Split(',').Select(item => item).ToList<string>();
//will not work for obvious reasons. Comma delimited FormCollection has commas to identify separate row data.
It seems like Microsoft designed the FormsCollection without the textarea control in mind. A text area with commas will not work when trying to access each value. What is interesting is that the _entriestables property has it in the perfect format but they chose to make it a private property. Very frustrating.
`
Here is the important part of my viewmodel.
public class TenantViewModel
{
public Tenant Tenant { get; set; }
public Site Site { get; set; }
}
My view is populated like this:
if (Model != null && Model.Tenant != null && Model.Tenant.Site != null && Model.Tenant.Site.Count() > 0)
{<div class="detailsbox_view">
<table id="tblTenantSites">
<tr>
<th>#Html.LabelFor(item => item.Site.Title)</th>
<th>#Html.LabelFor(item => item.Site.Description)</th>
</tr>
#foreach (var Item in Model.Tenant.Sites)
{
<tr>
#Html.HiddenFor(modelItem => Item.SiteId)
<td>
#Html.EditorFor(modelItem => Item.Title)
</td>
<td>
#Html.TextAreaFor(modelItem => Item.Description, new {#width="400" })
</td>
</tr> }
</table>
As you see this site table is a child of Tenant object. This child record does not get automatically updated using this method but the Tenant data does automatically get updated. This is the reason I tried the FormColelction instead.
Is there something I am missing to make this work?
try with this useful function
ValueProviderResult Match=FormCollection.GetValue("ValueProvider");
When you have multiple fields with the same name attribute, they'll come back into your FormCollection as an array. So upon posting a view like this:
<form action="/Home/MyAction">
<textarea id="row_one_description" name="description">
First row's description
</textarea>
<textarea id="row_two_description" name="description">
Second row's description
</textarea>
<input type="submit" value="Submit" />
</form>
you could do something like this in your action
[HttpPost]
public ActionResult MyAction(FormCollection collection)
{
var descriptionArray = collection["description"];
string firstRowDescription = descriptionArray[0];
string secondRowDescription = descriptionArray[1];
}
I must note that this is not the recommended way of dealing with posted data. You should instead be building your view using data from a view model and using strongly typed html helpers to render your controls. That way when you post, your action can take the ViewModel as a parameter. Its properties will be automatically bound and you will have a nice object to play with.
[HttpPost]
public ActionResult MyAction(MyViewModel viewModel)
{
foreach (var row in viewModel.Rows)
{
string description = row.Description;
}
}
EDIT
I'm still assuming a lot about your ViewModel but perhaps try this:
<table id="tblTenantSites">
<tr>
<th>#Html.LabelFor(model => model.Site.Title)</th>
<th>#Html.LabelFor(model => model.Site.Description)</th>
</tr>
#for (var i = i < Model.Tenants.Sites.Count(); i++) {
<tr>
#Html.HiddenFor(model => model.Tenants.Sites[i].SiteId)
<td>
#Html.EditorFor(model => model.Tenants.Sites[i].Title)
</td>
<td>
#Html.TextAreaFor(model => model.Tenants.Sites[i].Description, new { #width="400" } )
</td>
</tr>
}
</table>
You could also try ,
string Match=FormCollection.GetValue("ValueProvider").AttemptedValue;

Resources