I have an ASP.NET MVC 5 project where I need to get a class (with approximately 20 variables) from my controller to a view, and then I need to get the data back to my controller. I know I can use ViewBag variables, but that seems so tedious for that many variables, and I don't want to use a model because the data won't necessarily come from or go to a database. Does anyone know a good way to do this?
Use a view model. Models don't have to be connected to a database. They're very handy for usage in forms. Here's a very simplified example of what you can do:
public class PersonViewModel
{
public string Name {get;set;}
public string Email {get;set;}
// .... etc
}
Load form
[HttpGet]
public ActionResult MyForm()
{
return View(new PersonViewModel());
}
Post form
[HttpPost]
public ActionResult MyForm(PersonViewModel model)
{
if (ModelState.IsValid)
{
// do stuff
}
return View(model);
}
View (bound to PersonViewModel)
#model PersonViewModel
<h1>Person Form</h1>
#using (Html.BeginForm())
{
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
#Html.LabelFor(m => m.Email)
#Html.TextBoxFor(m => m.Email)
<button type="submit">Submit</button>
}
I have a view on which the user can log time spent on an Activity using an HTML form. So that view loops through a list of all Activities and generates a log time form (contained in the _LogTime partial view) for each one. The only piece of information passed to the partial view from the Index view is the ActivityId, which is placed in a hidden form. The rest of the required information is provided via the from by the user.
The problem I'm having is that once I submit one of the forms, the hidden field for all of the forms is set to the ActivityId of the form I just submitted. It's worth noting that when the page first loads (before I submit any forms), the hidden fields are correct, and when I submit a form for the first time, the correct Activity gets time logged to it (and none of the others erroneously get time logged). But any form submissions after that will only log time to the Activity I first submitted the form for.
Any idea what's going on here? Why are all of the hidden fields being set to the same ActivityId? And why only after the first POST? Let me know if you need any clarification of the problem.
Models:
public class Activity
{
public int ActivityId { get; set; }
public string Name { get; set; }
}
public class UserActivity
{
public int UserId { get; set; }
public int ActivityId { get; set; }
public int Duration { get; set; }
public DateTime Date { get; set; }
}
Views:
// Index View
#foreach (Activity activity in Model)
{
#Html.Partial("_LogTime", new UserActivity(activity.ActivityId))
}
// _LogTime Partial View
#using (Html.BeginForm())
{
<fieldset>
#Html.HiddenFor(model => model.ActivityId)
#Html.EditorFor(model => model.Duration)
#Html.EditorFor(model => model.Date)
<input type="submit" value="LOG TIME" />
</fieldset>
}
Controller:
public class ActivityController : Controller
{
private readonly DbContext _db = new DbContext();
public ActionResult Index()
{
return View(_db.Activities.ToList());
}
[HttpPost]
public ActionResult Index(UserActivity activity)
{
if (ModelState.IsValid)
{
_db.UserActivities.Add(activity);
_db.SaveChanges();
}
return View(_db.Activities.ToList());
}
}
What you are experiencing is due to the fact that the html helper methods automatically update form elements with post variables of the same name. The values are stored in ModelState. One way to fix this is to remove the offending entry from ModelState.
Another possible fix is to do a redirect instead.
[HttpPost]
public ActionResult Index(UserActivity activity)
{
if (ModelState.IsValid)
{
_db.UserActivities.Add(activity);
_db.SaveChanges();
}
// Remove the ActivityId from your ModelState before returning the View.
ModelState.Remove("ActivityId")
return View(_db.Activities.ToList());
}
As witnessed by the comments below, use of the Remove method can indicate a deeper issue with the flow of your application. I do agree with Erik on that point. As he points out, redesigning the flow of an application can be a time consuming task.
When encountering the behavior indicated by the question, if there is a way to solve the problem without modifying ModelState, that would be a preferred solution. A case in point might be where more than a single element were affected by this issue.
For completeness, here is an alternate solution:
[HttpPost]
public ActionResult Index(UserActivity activity)
{
if (ModelState.IsValid)
{
_db.UserActivities.Add(activity);
_db.SaveChanges();
}
return RedirectToAction("Index");
}
Towards the end of silencing my critic, here is the rewrite that he could not come up with.
// Index View
#using (Html.BeginForm())
{
#for (var i = 0; i < Model.Count; i++)
{
<div>
#Html.HiddenFor(model => model[i].ActivityId)
#Html.EditorFor(model => model[i].Duration)
#Html.EditorFor(model => model[i].Date)
</div>
}
<input type="submit" value="LOG TIME ENTRIES" />
}
// Controller Post Method
[HttpPost]
public ActionResult Index(List<UserActivity> activities)
{
if (ModelState.IsValid)
{
foreach( var activity in activities )
{
var first = _db.UserActivities
.FirstOrDefault(row => row.ActivityId == activity.ActivityId );
if ( first == null ) {
_db.UserActivities.Add(activity);
} else {
first.Duration = activity.Duration;
first.Date = activity.Date;
}
}
_db.SaveChanges();
return RedirectToAction("index");
}
// when the ModelState is invalid, we want to
// retain posted values and display errors.
return View(_db.Activities.ToList());
}
I never use global variables in my Controller.
I rather put all my hidden values, also those in the foreach partial view, in the form.
That way, you pass the entire list and add one after that.
Now I think that you pass an empty row and add the last one to that.
To be sure, you can put a breakpoint in the post function.
#using (Html.BeginForm())
{
// Index View
#foreach (Activity activity in Model)
{
#Html.Partial("_LogTime", new UserActivity(activity.ActivityId))
}
// _LogTime Partial View
<fieldset>
#Html.HiddenFor(model => model.ActivityId)
#Html.EditorFor(model => model.Duration)
#Html.EditorFor(model => model.Date)
<input type="submit" value="LOG TIME" />
</fieldset>
}
I have a strongly typed view with the following model.
public class ProductViewModel
{
public Product Product { get; set; }
public List<ProductOptionWithValues> ProductOptionsWithValues { get; set; }
}
public class ProductOptionWithValues
{
public ProductOption ProductOption;
public List<AllowedOptionValue> AllowedOptionValues;
}
I'm using this Model To populate a form where a user can select the options they want for a product.
This is the view.
#model AsoRock.Entities.ViewModels.ProductViewModel
#{
ViewBag.Title = "Details";
}
#using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<h3>
#Html.DisplayFor(model => model.Product.ProductName)
----> #Html.DisplayFor(model => model.Product.Price)
</h3>
<br/>
foreach (var item in Model.ProductOptionsWithValues)
{
<b>#Html.DisplayFor(modelItem => item.ProductOption.Option.OptionName)</b>
<br/>
#Html.DropDownListFor(m => m.ProductOptionsWithValues,
new SelectList(item.AllowedOptionValues,
"Id", "DisplayString",
item.AllowedOptionValues.First().Id))
<br/>
}
<input type="submit" value="Add to cart" />
}
In my controller I am trying to pass the model back. When I set a break point in the controller it hits it but the Product view model is empty, any ideas how I can get the values that are selected in the view back in to my controller?
[HttpPost]
public ActionResult Details(ProductViewModel ProductViewModel)
{
return View();
//return View();
}
As mentioned in the comments, you need to change the name of the viewmodel parameter from ProductViewModel to something else e.g.
[HttpPost]
public ActionResult Details(ProductViewModel viewModel)
{
}
Now it's very odd that the viewModel param is not set to an instance of the class. The MVC model binder will still create an instance of ProductViewModel even if none of it's properties are set to anything. You're not using a custom model binder by any chance?
Also I would very strongly suggest that your viewmodel class does not have a Product property. Instead, create properties in the viewmodel specifically for the Product properties you intend to use e.g.
public class ProductViewModel
{
public string ProductName { get; set; }
public decimal ProductPrice { get; set; }
public List<ProductOptionWithValues> ProductOptionsWithValues { get; set; }
}
Using Product in the viewmodel sort of defeats the point of having a viewmodel. The viewmodel should contain only the bare minimum that the view needs. Including Product means the viewmodel is now bloated with extra data it does not use/need.
EDIT:
In your shoes, I would strip down the view itself, using only little bits of the viewmodel, and POST to the controller to see what happens. If the viewmodel calss is not NULL, go back to the view and add another bit back. Keep doing this until the viewmodel is NULL again. Doing this bit by bit should help.
I can't seem to figure out how to send back the entire ViewModel to the controller to the 'Validate and Save' function.
Here is my controller:
[HttpPost]
public ActionResult Send(BitcoinTransactionViewModel transaction)
{
}
Here is the form in the view:
<li class="check">
<h3>Transaction Id</h3>
<p>#Html.DisplayFor(m => m.Transaction.TransactionId)</p>
</li>
<li class="money">
<h3>Deposited Amount</h3>
<p>#Model.Transaction.Amount.ToString() BTC</p>
</li>
<li class="time">
<h3>Time</h3>
<p>#Model.Transaction.Time.ToString()</p>
</li>
#using (Html.BeginForm("Send", "DepositDetails", FormMethod.Post, new { transaction = Model }))
{
#Html.HiddenFor(m => m.Token);
#Html.HiddenFor(m => m.Transaction.TransactionId);
#Html.TextBoxFor(m => m.WalletAddress, new { placeholder = "Wallet Address", maxlength = "34" })
<input type="submit" value="Send" />
#Html.ValidationMessage("walletAddress", new { #class = "validation" })
}
When i click on submit, the conroller contains the correct value of the walletAddress field but transaction.Transaction.Time, transaction.Transaction.Location, transaction.Transaction.TransactionId are empty.
Is there a way i could pass the entire Model back to the controller?
Edit:
When i dont even receive the walletAddress in the controller. Everything gets nulled!
When i remove this line alone: #Html.HiddenFor(m => m.Transaction.TransactionId);
it works and i get the Token property on the controller, but when i add it back, all the properties of the transaction object on the controller are NULL.
Here is the BitcoinTransactionViewModel:
public class BitcoinTransactionViewModel
{
public string Token { get; set; }
public string WalletAddress { get; set; }
public BitcoinTransaction Transaction { get; set; }
}
public class BitcoinTransaction
{
public int Id { get; set; }
public BitcoinTransactionStatusTypes Status { get; set; }
public int TransactionId { get; set; }
public decimal Amount { get; set; }
public DateTime Time { get; set; }
public string Location { get; set; }
}
Any ideas?
EDIT: I figured it out, its in the marked answer below...
OK, I've been working on something else and bumpend into the same issue all over again.
Only this time I figured out how to make it work!
Here's the answer for anyone who might be interested:
Apparently, there is a naming convention. Pay attention:
This doesn't work:
// Controller
[HttpPost]
public ActionResult Send(BitcoinTransactionViewModel transaction)
{
}
// View
#using (Html.BeginForm("Send", "DepositDetails", FormMethod.Post, new { transaction = Model }))
{
#Html.HiddenFor(m => m.Token);
#Html.HiddenFor(m => m.Transaction.TransactionId);
.
.
This works:
// Controller
[HttpPost]
public ActionResult Send(BitcoinTransactionViewModel **RedeemTransaction**)
{
}
// View
#using (Html.BeginForm("Send", "DepositDetails", FormMethod.Post, new { **RedeemTransaction** = Model }))
{
#Html.HiddenFor(m => m.Token);
#Html.HiddenFor(m => m.Transaction.TransactionId);
.
.
In other words - a naming convention error! There was a naming ambiguity between the Model.Transaction property and my transaction form field + controller parameter. Unvelievable.
If you're experiencing the same problems make sure that your controller parameter name is unique - try renaming it to MyTestParameter or something like this...
In addition, if you want to send form values to the controller, you'll need to include them as hidden fields, and you're good to go.
The signature of the Send method that the form is posting to has a parameter named transaction, which seems to be confusing the model binder. Change the name of the parameter to be something not matching the name of a property on your model:
[HttpPost]
public ActionResult Send(BitcoinTransactionViewModel model)
{
}
Also, remove the htmlAttributes parameter from your BeginForm call, since that's not doing anything useful. It becomes:
#using (Html.BeginForm("Send", "DepositDetails", FormMethod.Post))
Any data coming back from the client could have been tampered with, so you should only post back the unique ID of the transaction and then retrieve any additional information about it from your data source to perform further processing. You'll also want to verify here that the user posting the data has access to the specified transaction ID since that could've been tampered with as well.
This isn't MVC specific. The HTML form will only post values contained within form elements inside the form. Your example is neither inside the form or in a form element (such as hidden inputs). You have to do this since MVC doesn't rely on View State. Put hidden fields inside the form:
#Html.HiddenFor(x => x.Transaction.Time)
// etc...
Ask yourself though.. if the user isn't updating these values.. does your action method require them?
Model binding hydrates your view model in your controller action via posted form values. I don't see any form controls for your aforementioned variables, so nothing would get posted back. Can you see if you have any joy with this?
#using (Html.BeginForm("Send", "DepositDetails", FormMethod.Post, new { transaction = Model }))
{
#Html.TextBoxFor(m => m.WalletAddress, new { placeholder = "Wallet Address", maxlength = "34" })
#Html.Hidden("Time", Model.Transaction.Time)
#Html.Hidden("Location", Model.Transaction.Location)
#Html.Hidden("TransactionId", Model.Transaction.TransactionId)
<input type="submit" value="Send" />
#Html.ValidationMessage("walletAddress", new { #class = "validation" })
}
Try to loop with the folowing statement not with FOREACH
<table>
#for (var i = 0; i < Model.itemlist.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(x => x.itemlist[i].Id)
#Html.HiddenFor(x => x.itemlist[i].Name)
#Html.DisplayFor(x => x.itemlist[i].Name)
</td>
</tr>
}
</table>
Try Form Collections and get the value as. I think this may work.
public ActionResult Send(FormCollection frm)
{
var time = frm['Transaction.Time'];
}
Put all fields inside the form
#using (Html.BeginForm("Send", "DepositDetails", FormMethod.Post))
and make sure that the model
BitcoinTransactionViewModel
included in view or not?
Can you just combine those 2 models you have? Here's how I do it with one model per view...
1. I use Display Templates from view to view so I can pass the whole model as well as leave data encrypted..
2. Setup your main view like this...
#model IEnumerable<LecExamRes.Models.SelectionModel.GroupModel>
<div id="container">
<div class="selectLabel">Select a Location:</div><br />
#foreach (var item in Model)
{
#Html.DisplayFor(model=>item)
}
</div>
3. Create a DisplayTemplates folder in shared. Create a view, naming it like your model your want to pass because a DisplayFor looks for the display template named after the model your using, I call mine GroupModel. Think of a display template as an object instance of your enumeration. Groupmodel Looks like this, I'm simply assigning a group to a button.
#model LecExamRes.Models.SelectionModel.GroupModel
#using LecExamRes.Helpers
#using (Html.BeginForm("Index", "Home", null, FormMethod.Post))
{
<div class="mlink">
#Html.AntiForgeryToken()
#Html.EncryptedHiddenFor(model => model.GroupKey)
#Html.EncryptedHiddenFor(model => model.GroupName)
<p>
<input type="submit" name="gbtn" class="groovybutton" value=" #Model.GroupKey ">
</p>
</div>
}
4. Here's the Controller.
*GET & POST *
public ActionResult Index()
{
// Create a new Patron object upon user's first visit to the page.
_patron = new Patron((WindowsIdentity)User.Identity);
Session["patron"] = _patron;
var lstGroups = new List<SelectionModel.GroupModel>();
var rMgr = new DataStoreManager.ResourceManager();
// GetResourceGroups will return an empty list if no resource groups where found.
var resGroups = rMgr.GetResourceGroups();
// Add the available resource groups to list.
foreach (var resource in resGroups)
{
var group = new SelectionModel.GroupModel();
rMgr.GetResourcesByGroup(resource.Key);
group.GroupName = resource.Value;
group.GroupKey = resource.Key;
lstGroups.Add(group);
}
return View(lstGroups);
}
[ValidateAntiForgeryToken]
[HttpPost]
public ActionResult Index(SelectionModel.GroupModel item)
{
if (!ModelState.IsValid)
return View();
if (item.GroupKey != null && item.GroupName != null)
{
var rModel = new SelectionModel.ReserveModel
{
LocationKey = item.GroupKey,
Location = item.GroupName
};
Session["rModel"] = rModel;
}
//So now my date model will have Group info in session ready to use
return RedirectToAction("Date", "Home");
}
5. Now if I've got alot of Views with different models, I typically use a model related to the view and then a session obj that grabs data from each model so in the end I've got data to submit.
The action name to which the data will be posted should be same as the name of the action from which the data is being posted. The only difference should be that the second action where the data is bein posted should have [HttpPost] and the Posting method should serve only Get requests.
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.