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 have created a mobile store project. There is a dropdown in my view, I need to fill this dropdown from database so I have declared a public variable in controller but it's not accessible in views. I have also tried to declare that in model class, it's working fine, but problem is that when I update the entity model from database these variable got deleted. So please suggest me how to do that and also I don't want to use ViewBag. Code which I have tried.
public SelectList Vendor { get; set; }
public ActionResult ShowAllMobileDetails(tbInsertMobile IM)
{
Vendor = new SelectList(db.Usp_VendorList(), "VendorId", "VendorName");
return View(IM);
}
Views..
#model MobileApplicationEntityFramework.Models.tbInsertMobile
#{
ViewBag.Title = "ShowAllMobileDetails";
}
#using (#Html.BeginForm())
{
<h2>ShowAllMobileDetails</h2>
<p>
#Html.ActionLink("Create New Mobile", "InsertMobile");
</p>
<fieldset>
<legend>Search Panel</legend>
<table>
<tr>
<td>Mobile Name</td>
<td>#Html.TextBoxFor(a => a.MobileName) </td>
</tr>
<tr>
<td>Mobile Manufacured</td>
<td>#Html.DropDownList("ddlVendor", Vendor, "Select Manufacurer")</td>
</tr>
</table>
</fieldset>
}
hi: you can define a static property to share to all place in your project:
public static class Global
{
public static SelectList Vendor()
{
return (new SelectList(db.Usp_VendorList(), "VendorId", "VendorName"));
}
}
and use in your views like this:
Mobile Manufacured
#Html.DropDownList("ddlVendor", namespaceOfGlobal.Vendor, "Select Manufacurer")
I have a model, MyViewModel, with some members (not all are included).
namespace ANameSpace
{
public class MyViewModel
{
public int ID { get; set }
public EditTableObject EditTable { get; set }
public List<EditTableObject> EditTables { get; set }
// some other members
}
public class EditTableObject
{
public int ID { get; set }
// some other members
}
}
In the controller there are two ActionResult Index() methods
In The first, Index(int? id), I assign a value to model.ID and also add some EditTableObjects to an EditTables list.
Then, in the second, Index(ViewModel tmpModel), I am trying to use the members that I gave values to in the first index method. However, the ID member does not contain the value I assigned it; it's null. But strangely, the EditTables list does contain the values I assigned to it.
Here is some code from the controller:
namespace AnotherNamespace
{
public class MyController
{
public ActionResult Index(int? id)
{
if (id != null)
{
model.ID = (int)id;
}
else
{
model.ID = 1000;
}
model.EditTable = new EditTableObject();
model.EditTable.ID = model.ID;
model.EditTables.Add(model.EditTable);
return View(model);
}
public ActionResult(MyViewModel tmpModel)
{
return RedirectToAction("Index", new { id = tmpModel.ID });
}
}
}
If I set a break point on the second Index method I find that all the EditTables data persists but not the model.ID data.
Why is the EditTables data persisting but not the model.ID data?
Thanks for any suggestions.
Eventhough I don't quite get your question (posting some code that doesn't work will highly improve your chances of getting an answer) I'll give it a shot.
HttpRequests are stateless. So between the calls to the different methods you need to persist your data. Usually that's been done using a database.
This is a good source to get started with MVC: http://www.asp.net/mvc
Because you are not giving the entire model.
In order to receive the model you need to set your view based on that model and pass it to the controller on httppost.
Example
Controller
public ActionResult Create()
{
List<EditTableObject> myList = ....*YourList*....
ViewBag.EditTables = myList;
ViewModel vm = new ViewModel();
return View(vm);
}
[HttpPost]
public ActionResult Create(ViewModel myViewModel)
{
if(ModelState.IsValid)
{
// DO SOMETHING
}
else
{
return View(myViewModel);
}
}
View
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<namespace.ViewModel>" %>
<% using (Html.BeginForm()) { %>
<%: Html.AntiForgeryToken() %>
<%: Html.ValidationSummary(true) %>
<%: Html.HiddenFor(model => model.ID) %>
<div class="editor-field">
<%: Html.DropDownList("TableObject", IEnumerable<SelectListItem>)ViewBag.EditTable,String.Empty)%>
</div>
<% } %>
Okay, I figured it out.
In the the Razor View I added #Html.HiddenFor(modelItem => Model.ID) to a table row.
Here it is in more context:
#for (int i = 0; i < Model.EditTables.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(modelItem => modelItem.EditTables[i].ID)
#Html.HiddenFor(modelItem => Model.ID)
</td>
</tr>
}
Sorry for my lack of code in the beginning. I work for a company that would not be happy I shared code on the internet so I have to pick and choose and be careful what I post.
I have two models:
class ModelIn{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}
class ModelOut{
public ModelOut(){ People = new List<ModelIn>();}
public List<ModelIn> People { get; private set;}
public string Country { get; set; }
}
And I have Controller editing ModelOut:
public ActionResult People()
{
...
return View(SomeModelOutInstanceWith3People);
}
[HttpPost]
public ActionResult(ModelOut items)
{
...
}
In view I have sth like:
<% using (Html.BeginForm()) { %>
<%: Html.EditorFor(m => Model.Country) %>
<% for(int i = 0; i < Model.People.Count; ++i){ %>
<%: Html.EditorFor(m => Model.People[i].FirstName) %>
<%: Html.EditorFor(m => Model.People[i].LastName) %>
<%: Html.EditorFor(m => Model.People[i].Address) %>
<% } %>
<input type="submit" />
<% } %>
It works all OK, but in post action I have empty ModelOut items.
I can see in logs that data is sent correctly.
I have tried everything, nothing works.
Have you tried simply <%: Html.EditorFor(m => m.People) %> ?
MVC should loop through the list by itself.
Also, watch out how you specify your lambdas, it should be m => m rather than m => Model.
PS. I use MVC3...
The reason for your problem could be a naming mismatch...from what I remember the default model binder does not do its job properly because of this naming mismatch....this means you need to specify more info to the model binder to do its job better... try updating your View code to use the below code for each property...
<%= Html.EditorFor(string.Format("People[{0}].FirstName", i), Model.People[i].FirstName) %>
The above view code will generate the following markup
<input id="People_0__FirstName" name="People[0].FirstName" type="text" />
I might have a syntactical problem above but I guess you can get it right with the help of Visual Studio
#Dai were right. MVC let me use items for model instance name when it is instance of List, but doesn't let me use it for ModelOut.
After renaming items to model it works fine.
My ViewResult Controller:
public ViewResult Controller(int id)
{
List<Data> dataList = dataAccess.getDataById(id);
Results[] resultArray = new Results[dataList.Count];
ViewBag.results= resultArray;
return View(dataList);
}
My View:
#model IEnumerable<Solution.Models.Data>
#{
Solution.Models.Results[] res= ViewBag.results;
}
#using (Html.BeginForm()) {
<table>
#{
int i = 0;
foreach (var item in Model) {
<tr>
<td>
Snippet: #Html.DisplayFor(modelItem => item.text)
</td>
</tr>
<tr>
<td>
Translation: #Html.EditorFor(modelItem => res[i].text)
</td>
</tr>
i++;
}
}
</table>
<p>
<input class="CRUD-buttons" type="submit" value="Send" />
</p>
}
My Controller ActionResult:
[HttpPost]
public ActionResult Controller(/*List<Data> dataList, */Results[] results)
{
ResultText = results[0].text; //NullReferenceException
}
dataList and results are empty. I read a couple of posts on stackoverflow, but could not find a solution.
I took already a look on the following blog (link) but its MVC 2 code. :(
There are multiple ways to do this. What gets you here is that in order for you to receive the results parameter, the name for the generated edit controls should be results[0] for the first, results[1] for the second, etc with no gaps in the indexes (this is because of how DefaultModelBinder expects to find the fields named when the form is posted).
So one immediate (although not very good) solution would be to specify the name correctly:
#Html.TextBox(string.Format("results[{0}]", i), res[i].text)
A much better solution would be to put the results into your model (better yet, in a ViewModel created specifically for this view). For example, first of all you create a class that encapsulates one piece of data and the corresponding result:
class ItemViewModel
{
Solution.Models.Data TheData { get; set; }
Solution.Models.Results TheResults { get; set; }
}
You then make your view have a collection of these items as its model:
#model IEnumerable<ItemViewModel>
and then output the edit control with
Translation: #Html.EditorFor(modelItem => modelItem.TheResults)
Finally, you modify the postback action to accept an array of ItemViewModel:
[HttpPost]
public ActionResult Controller(ItemViewModel[] results)
General piece of advice: Try to avoid using EditorFor with objects that are not part of your view's model (for example, things you have passed through ViewBag or ViewData). If you do this, MVC will punish you.