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:
[HttpGet]
[Route]
ViewBag
[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.
[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
.
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?
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!
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.
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; }
}
}
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
}
}
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>
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: