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.
Related
I've seen many tutorials and documentation pass the model as an argument in an Action in a controller. Every time I do this I get a 415 status error (incorrect media type) when the action is called. This is problematic for me because my fields clear after the action occurs. Many have suggested calling the model when I return the View, but that has not been working for me. Does anyone know why that is and how I can fix it? I'm so frustrated I've tried so many things and it just never works :(
Example of how I want to pass the model as an argument:
[HttpGet("[action]")]
public async Task<IActionResult> Search(Movies model, int ID, string titleSearch,
string genreSearch)
{
return View(model);
}
My View:
#model IEnumerable<MyApp.Models.Movies>
#{
ViewData["Title"] = "Movies";
}
<form method="get" role="form" asp-controller="MoviesList" asp-action="Index">
<label>Movie Genre</label>
<select name="movieGenre" asp-items="#(new SelectList(ViewBag.genre, "ID", "Genre"))"></select>
<label>Movie Title</label>
<input type="search" value="#ViewData["movieTitle"]" name="movieTitle" />
<input type="submit" value="Search" asp-controller="MoviesList" asp-action="Search" />
</form>
<input type="hidden" name="ID" value="#ViewBag.pageID"
<table>
<thead>
<tr>
<th>
#Html.DisplayNameFor(m => m.Title)
</th>
<th>
#Html.DisplayNameFor(m => m.Genre)
</th>
</tr>
</thead>
<tbody>
#foreach(var item in Model)
{
<tr>
<th>
#Html.DisplayFor(modelItem => item.Title)
</th>
<th>
#Html.DisplayFor(modelItem => item.Genre)
</th>
</tr>
}
</tbody>
</table>
My Controller:
//This action is called when the page is first called
[HttpGet("[action]")]
[Route("/MoviesList/Index/id")]
public async Task<IActionResult> Index(int id)
{
//using ViewBag to set the incoming ID and save it in the View
//so that I can access it from my search action
ViewBag.pageID = id;
//calling a query to load data into the table in the View
//var query = query
return View(await query);
}
//searching the movies list with this action
[HttpGet("[action]")]
public async Task<IActionResult> Search(int ID, string titleSearch, string genreSearch)
{
int id = ID;
ViewData["titleSearch"] = titleSearch;
//do some necessary conversions to the incoming data (the dropdowns for example come in as
//integers that match their value in the DB
var query = from x in _db.Movies
.Where(x => x.Id == id)
select x;
//some conditionals that check for null values
//run the search query
query = query.Where(x =>
x.Title.Contains(titleSearch) &&
x.Genre.Contains(genreSearch));
//when this return happens, I do get all of my results from the search,
//but then all of the fields reset & my hidden ID also resets
//this is problematic if the user decides they want to search again with
//different entries
return View("Index", await query.AsNoTracking().ToListAsync());
}
Overall, my goal is to not have any of the fields clear after my action is complete, and allow the user to re-call the action with new entries. From my understanding, passing the model as an argument can help me achieve my goal, but I haven't had any luck. Please let me know how I can achieve this goal. Thank you for your time!
There are so many things wrong in your code. I am not sure where to start but will try my best to list out a few:
Use of [HttpGet]
Use of Attribute Routing, [Route]
Form post
Overuse of ViewBag
1. Use of [HttpGet]
I don't want to say the way you used [HttpGet] passing a name as the parameter is wrong, but your setup will always ignore the controller name!
The [action] you passed in is call token replacement, which will be replaced with the value of the action name so:
/*
* [HttpGet("[action]")] on Search action => [HttpGet("search")] => matches /search
* [HttpGet("[action]")] on Index action => [HttpGet("index")] => matches /index
*/
See how wrong that is! You're missing the controller name!
A request /moviesList/index will not call the Index method from the MoviesList controller, but a request /index will!
Just take out the template/token replacement parameter. And by default, if you don't mark the controller action with any HTTP verb templates, i.e., [HttpGet], they're default to handle HTTP GET requests.
2. Use of Attribute Routing, [Route]
I don't want to say using attribute routing in a Model-View-Controller application is wrong, but attribute routing is used mostly when you're building a RESTful API application.
By default, the app is setup to use the conventional routing, which should come with the template when you first create your application:
namespace DL.SO.SearchForm.WebUI
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
The way you used [Route] attribute gives me an impression that you don't know what they're or at least you are confused. With the conventional routing, even if you don't put [Route] on the controllers, the following requests should arrive to their corresponding controller actions by the "default" routing:
/*
* /moviesList/index GET => MoviesList controller, Index action
* /moviesList/search GET => MoviesList controller, Search action
*/
By the way, a controller named MoviesListController is awful. I will just call it MovieController.
3. Form Post
Within the form, you can't specify a controller and the action on the submit button. It's not an anchor tag anyway.
And <input type="hidden" name="ID" value="#ViewBag.pageID" is outside the form. How would the form know what that is and post the correct value back?
4. Overuse of ViewBag / ViewData
Technically you can only use ViewBag to transfer data between controller to view. ViewData is only valid in the current request, and you can only transfer data from controller to view, not vice-versa.
In additional, they're so-called weakly typed collections. They're designed to transfer small amount of data in and out of controllers and views, like the page title. If you overuse them, your applications will become so hard to maintain as you have to remember what type the data is when using it.
By overusing ViewBag / ViewData, you're basically removing one of the best features about C# & Razor - strongly typed.
The best approach is to specify a view model in the view. You pass an instance of the view model to the view from the controller action. The view model defines only the data the view needs! You should not pass your entire database model to the view so that users can use your other important information!
My approach
Instead of using a single method to handle listing all the movies as well as the search filters, I would like to separate them. The search form will be using [HttpPost] instead of [HttpGet].
That way I will only need to post back the search filters data, and I can now define custom parameters on the Index action and have the Post action redirect to the Index action.
I will show you what I mean.
View Models
First I will define all the view models I need for the view:
namespace DL.SO.SearchForm.WebUI.Models.Movie
{
// This view model represents each summarized movie in the list.
public class MovieSummaryViewModel
{
public int MovieId { get; set; }
public string MovieTitle { get; set; }
public string MovieGenre { get; set; }
public int MovieGenreId { get; set; }
}
// This view model represents the data the search form needs
public class MovieListSearchViewModel
{
[Display(Name = "Search Title")]
public string TitleSearchQuery { get; set; }
[Display(Name = "Search Genre")]
public int? GenreSearchId { get; set; }
public IDictionary<int, string> AvailableGenres { get; set; }
}
// This view model represents all the data the Index view needs
public class MovieListViewModel
{
public MovieListSearchViewModel Search { get; set; }
public IEnumerable<MovieSummaryViewModel> Movies { get; set; }
}
}
The Controller
Next, here comes the controller:
One thing to pay attention here is that you have to name the POST action parameter the same way as you define it in the view model, like so MovieListSearchViewModel search.
You can't name the parameter name something else because we're posting partial view model back to MVC, and by default, the model binding will only bind the data for you if it matches the name.
namespace DL.SO.SearchForm.WebUI.Controllers
{
public class MovieController : Controller
{
// See here I can define custom parameter names like t for title search query,
// g for searched genre Id, etc
public IActionResult Index(string t = null, int? g = null)
{
var vm = new MovieListViewModel
{
Search = new MovieListSearchViewModel
{
// You're passing whatever from the query parameters
// back to this search view model so that the search form would
// reflect what the user searched!
TitleSearchQuery = t,
GenreSearchId = g,
// You fetch the available genres from your data sources, although
// I'm faking it here.
// You can use AJAX to further reduce the performance hit here
// since you're getting the genre list every single time.
AvailableGenres = GetAvailableGenres()
},
// You fetch the movie list from your data sources, although I'm faking
// it here.
Movies = GetMovies()
};
// Filters
if (!string.IsNullOrEmpty(t))
{
// Filter by movie title
vm.Movies = vm.Movies
.Where(x => x.MovieTitle.Contains(t, StringComparison.OrdinalIgnoreCase));
}
if (g.HasValue)
{
// Filter by movie genre Id
vm.Movies = vm.Movies
.Where(x => x.MovieGenreId == g.Value);
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
// You have to name the paramter "Search" as you named so in its parent
// view model MovieListViewModel
public IActionResult Search(MovieListSearchViewModel search)
{
// This is the Post method from the form.
// See how I just put the search data from the form to the Index method.
return RedirectToAction(nameof(Index),
new { t = search.TitleSearchQuery, g = search.GenreSearchId });
}
#region Methods to get fake data
private IEnumerable<MovieSummaryViewModel> GetMovies()
{
return new List<MovieSummaryViewModel>
{
new MovieSummaryViewModel
{
MovieId = 1,
MovieGenreId = 1,
MovieGenre = "Action",
MovieTitle = "Hero"
},
new MovieSummaryViewModel
{
MovieId = 2,
MovieGenreId = 2,
MovieGenre = "Adventure",
MovieTitle = "Raiders of the Lost Ark (1981)"
},
new MovieSummaryViewModel
{
MovieId = 3,
MovieGenreId = 4,
MovieGenre = "Crime",
MovieTitle = "Heat (1995)"
},
new MovieSummaryViewModel
{
MovieId = 4,
MovieGenreId = 4,
MovieGenre = "Crime",
MovieTitle = "The Score (2001)"
}
};
}
private IDictionary<int, string> GetAvailableGenres()
{
return new Dictionary<int, string>
{
{ 1, "Action" },
{ 2, "Adventure" },
{ 3, "Comedy" },
{ 4, "Crime" },
{ 5, "Drama" },
{ 6, "Fantasy" },
{ 7, "Historical" },
{ 8, "Fiction" }
};
}
#endregion
}
}
The View
Finally here comes the view:
#model DL.SO.SearchForm.WebUI.Models.Movie.MovieListViewModel
#{
ViewData["Title"] = "Movie List";
var genreDropdownItems = new SelectList(Model.Search.AvailableGenres, "Key", "Value");
}
<h2>Movie List</h2>
<p class="text-muted">Manage all your movies</p>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<form method="post" asp-area="" asp-controller="movie" asp-action="search">
<div class="form-group">
<label asp-for="Search.GenreSearchId"></label>
<select asp-for="Search.GenreSearchId"
asp-items="#genreDropdownItems"
class="form-control">
<option value="">- select -</option>
</select>
</div>
<div class="form-group">
<label asp-for="Search.TitleSearchQuery"></label>
<input asp-for="Search.TitleSearchQuery" class="form-control" />
</div>
<button type="submit" class="btn btn-success">Search</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
#if (Model.Movies.Any())
{
foreach (var movie in Model.Movies)
{
<tr>
<td>#movie.MovieId</td>
<td>#movie.MovieTitle</td>
<td>#movie.MovieGenre</td>
</tr>
}
}
else
{
<tr>
<td colspan="3">No movie matched the searching citiria!</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
Screenshots
When you first land on the Movies page:
The available Genre list as well as the movie list is shown correctly:
Search by Genre:
Search by Title:
You don't really "pass arguments" to a controller action - you're issuing HTTP requests to an endpoint defined by your application, which the various middleware running in your app attempt to process. In this case, one of those middlewares is the MVC framework/module, which tries to map route values (controller, action, etc.) to the matching classes, and query string or form values where relevant.
Since you've defined that Search action as only matching GET requests, you're reading from the query string (the ?foo=bar&bar=baz content you typically see in your navigation bar). A C# class is not something you can send as a query string value (there are ways around this, using attributes, but that's kind of overkill for your example). I'd read https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1 if you haven't already.
The Search action in your last example will work, but you've rendered the input outside the <form> element; for it to be included, you either need to render it inside the form or use the form="form id here" attribute to associate it with that form (you'll need to add an id="something" attribute to the form for that to work, as well).
<form method="get" role="form" asp-controller="MoviesList" asp-action="Index">
<label>Movie Genre</label>
<select name="movieGenre" asp-items="#(new SelectList(ViewBag.genre, "ID", "Genre"))"></select>
<label>Movie Title</label>
<input type="search" value="#ViewData["movieTitle"]" name="movieTitle" />
<input type="submit" value="Search" asp-controller="MoviesList" asp-action="Search" />
<input type="hidden" name="ID" value="#ViewBag.pageID" />
</form>
You have two choices (well, more actually, but let's say two for now) if you want to retain the values used to submit your search form:
Add the querystring values to ViewBag/ViewData (you started to do this)
Use an actual view model, rather than a collection of values
I'd personally go with #2, since it also makes your view cleaner to bind. So:
public class SearchViewModel
{
public SearchViewModel()
{
Matches = Array.Empty<Movies>();
Genres = Array.Empty<Genre>();
}
public int? ID { get; set; }
public string Title { get; set; }
public string Genre { get; set; }
public IEnumerable<Movies> Matches { get; set; }
public IEnumerable<Genre> Genres { get; set; }
}
View:
#model SearchViewModel
#{
ViewData["Title"] = "Movies";
}
<form method="get" role="form" asp-controller="MoviesList" asp-action="Index">
<label>Movie Genre</label>
<select asp-for="Genre" asp-items="#(new SelectList(Model.Genres, "ID", "Genre"))"></select>
<label>Movie Title</label>
<input type="search" asp-for="Title" />
<button>Search</button>
<input type="hidden" asp-for="ID" />
</form>
<table>
<thead>
<tr>
<th>
Title
</th>
<th>
Genre
</th>
</tr>
</thead>
<tbody>
#foreach(var item in Model.Matches)
{
<tr>
<td>
#item.Title
</td>
<td>
#item.Genre
</td>
</tr>
}
</tbody>
</table>
Controller
If you make your action parameters nullable, you actually only need one action for both the "default" action and search:
[HttpGet("[action]")]
[Route("/MoviesList/Index/id")]
public async Task<IActionResult> Index(int? id, string title = null, string genre = null)
{
var model = new SearchViewModel();
// ... add code for populating model.Genres...
var query = _db.Movies.AsQueryable();
if(id != null)
{
model.ID = id.value;
query = query.Where(m => m.ID == id);
}
if(title != null)
{
model.Title = title;
query = query.Where(m => m.Title.Contains(title));
}
if(genre != null)
{
model.Genre = genre;
query = query.Where(m => m.Genre.Contains(Genre));
}
model.Matches = await query
.OrderBy(m => m.Title)
.ToListAsync();
return View(model);
}
This is completely untested, so caveat emptor.
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.
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);
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 use asp net MVC 3 one of my project.I use partial view for my coding. I want to list all customers in a list and submit their information as a list. When I try to submit my list in post back, it sends my list is null. You can find my code as in the below:
My controller method is:
[HttpPost]
public ActionResult ConfirmUsers(ICollection<Career.DomainModel.UserApprovalDto> collection)
{
string bas = "";
//if (collection != null)
if (ModelState.IsValid)
{
bas = "bas";
}
return RedirectToAction("Index");
}
My partial view is:
#model List<Career.DomainModel.UserApprovalDto>
#using (Html.BeginForm("ConfirmUsers", "ManageUsers", new { area = "" }, FormMethod.Post))
{
<table>
<tr>
<th>
Name
</th>
<th>
Is Reported
</th>
</tr>
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>
#Html.DisplayFor(modelItem => Model[i].FirstName)
</td>
<td>
#Html.CheckBox("IsReported", Model[i].IsReported.HasValue ? Model[i].IsReported.Value : false)
#*#Html.CheckBoxFor(modelItem => Model[i].IsReported.Value);*# #* #if (Model[i].IsReported != null)
{
#Html.CheckBoxFor(modelItem => Model[i].IsReported.Value);
}
else
{
#Html.CheckBoxFor(modelItem => Model[i].IsReported.Value);
}*#
</td>
<td>
</td>
</tr>
}
</table>
<div>
<input name="submitUsers" type="submit" value="Save" />
</div>
}
Thanks in advance.
Kerem
I would use Editor template to handle this. Have your View Model like this to represent the CheckBox item.
public class ReportedUserViewModel
{
public string FirstName { set;get;}
public int Id { set;get;}
public bool IsSelected { set;get;}
}
Now in yout main view model, add a property which is a collection of the above class
public class ConfirmUserViewModel
{
public List<ReportedUserViewModel> ReportedUsers{ get; set; }
//Other Properties also here
public ConfirmUserViewModel()
{
ReportedUsers=new List<ReportedUserViewModel>();
}
}
Now in your GET Action, you will fill the values of the ViewModel and sent it to the view.
public ActionResult ConfirmUser()
{
var vm = new ConfirmUserViewModel();
//The below code is hardcoded for demo. you mat replace with DB data.
vm.ReportedUsers.Add(new ReportedUserViewModel { Name = "Test1" , Id=1});
vm.ReportedUsers.Add(new ReportedUserViewModel { Name = "Test2", Id=2 });
return View(vm);
}
Now Let's create an EditorTemplate. Go to Views/YourControllerName and Crete a Folder called EditorTemplate and Create a new View there with the same name as of the Property Name(ReportedUsers.cshtml)
Add this code to the newly created editor template.
#model ReportedUserViewModel
<p>
<b>#Model.FirstName </b> :
#Html.CheckBoxFor(x => x.IsSelected) <br />
#Html.HiddenFor(x=>x.Id)
</p>
Now in your Main View, Call your Editor template using the EditorFor Html Helper method.
#model ConfirmUserViewModel
#using (Html.BeginForm())
{
<div>
#Html.EditorFor(m=>m.ReportedUsers)
</div>
<input type="submit" value="Submit" />
}
Now when You Post the Form, Your Model will have the ReportedUsers Collection where the Selected Check boxes will be having a True value for the IsSelected Property.
[HttpPost]
public ActionResult AddAlert(ConfirmUserViewModel model)
{
if(ModelState.IsValid)
{
//Check for model.ReportedUsers collection and Each items
// IsSelected property value.
//Save and Redirect(PRG pattern)
}
return View(model);
}
With the code you wrote, MVC model binder mechanism does not know how to map those inputs into List of object.
Do this little trick instead:
#Html.CheckBox("[" + i + "]." + "IsReported", Model[i].IsReported.Value);
This will result the name of input field as [0].IsReported for the first item in the list, [1].IsReported for next item.
That should work.
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
I'm a little late to the party, but it is easy enough to just refer to the list if you embed it in the model--
#Html.CheckBoxFor(modelItem => Model.List[i].Selected)
That will post back for each item in the iterator.
#Html.CheckBox("[" + i + "]." + "IsReported", Model[i].IsReported.Value);
worked perfectly for me.
Make sure your post method contains parameter for the list.
e.g public ActionResult Index(ConfigViewModel model, List configurationList)
Here in my case I have one view model (model) which contains list object.
If you specify view model in your post method as it is then you will get null value for the list (here model object is null). But if you add specific list parameter (configurationList) in the action then you can get all of the list values in the controller.
i run into same problem last week.
I realize that checkbox has three value(true/false/null) when i get it from database because i let the checkbox value nullable when i desingned database. i redesinged db and the problem was solved.
you didnt post models so i dont realy sure if this is the case. just look at the model, if it's writing Nullable above your Ischeck property, go to database and redesign. remember isCheck, isSelected properties have to have just two values(true/false).