在 ASP.NET Core 3.1 MVC 中将模型传递到控制器操作时的 415 状态
415 Status When Passing Model into Controller Action in ASP.NET Core 3.1 MVC
我看过许多教程和文档将模型作为参数传递到控制器的 Action 中。每次执行此操作时,都会在调用操作时收到 415 状态错误(媒体类型不正确)。这对我来说是有问题的,因为我的字段在操作发生后清除。许多人建议在我 return 视图时调用模型,但这对我不起作用。有谁知道这是为什么以及我该如何解决?我很沮丧,我尝试了很多东西,但都没有用:(
我想如何将模型作为参数传递的示例:
[HttpGet("[action]")]
public async Task<IActionResult> Search(Movies model, int ID, string titleSearch,
string genreSearch)
{
return View(model);
}
我的看法:
@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>
我的控制器:
//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());
}
总的来说,我的目标是在我的操作完成后不清除任何字段,并允许用户使用新条目重新调用操作。据我了解,将模型作为参数传递可以帮助我实现我的目标,但我没有任何运气。请让我知道如何实现这个目标。感谢您的宝贵时间!
您实际上并没有向控制器操作“传递参数”——您是在向应用程序定义的端点发出 HTTP 请求,应用程序中的各种中间件 运行 会尝试处理该端点。在这种情况下,其中一个中间件是 MVC framework/module,它试图将路由值(控制器、操作等)映射到匹配的 classes,并在相关时查询字符串或表单值。
由于您已将该搜索操作定义为仅匹配 GET 请求,因此您正在从查询字符串(您通常在导航栏中看到的 ?foo=bar&bar=baz
内容)中读取。 C# class 不是您可以作为查询字符串值发送的东西(有一些方法可以解决这个问题,使用属性,但这对您的示例来说有点矫枉过正)。如果您还没有阅读,我会阅读 https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1。
上一个示例中的搜索操作将起作用,但您已将输入呈现在 <form>
元素之外;要包含它,您需要在表单内呈现它或使用 form="form id here"
属性将其与该表单相关联(您需要向表单添加一个 id="something"
属性,以便工作,以及)。
<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>
如果您想保留用于提交搜索表单的值,您有两个选择(好吧,实际上更多,但我们现在假设两个):
- 将查询字符串值添加到 ViewBag/ViewData(你开始这样做了)
- 使用实际的视图模型,而不是值的集合
我个人会选择#2,因为它还可以使您的视图更易于绑定。所以:
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; }
}
查看:
@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>
控制器
如果您将操作参数设置为可为空,则实际上“默认”操作和搜索都只需要一个操作:
[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);
}
这完全未经测试,所以买者自负。
您的代码中有很多错误。我不确定从哪里开始,但会尽力列出一些:
- 使用
[HttpGet]
- 属性路由的使用,
[Route]
- 表格post
- 过度使用
ViewBag
1。使用 [HttpGet]
我不想说你使用 [HttpGet]
传递名称作为参数的方式是错误的,但你的设置将始终忽略控制器名称!
你传入的[action]
是call token replacement,会替换成action name的值所以:
/*
* [HttpGet("[action]")] on Search action => [HttpGet("search")] => matches /search
* [HttpGet("[action]")] on Index action => [HttpGet("index")] => matches /index
*/
看看这是多么错误!您缺少控制器名称!
请求 /moviesList/index
不会从 MoviesList 控制器调用 Index 方法,但请求 /index
会!
把template/token替换参数去掉就行了。默认情况下,如果您不使用任何 HTTP 动词模板标记控制器操作,即 [HttpGet]
,它们将默认处理 HTTP GET 请求。
2。使用属性路由,[Route]
我不想说在 Model-View-Controller 应用程序中使用属性路由是错误的,但属性路由主要用于构建 RESTful API 应用程序。
默认情况下,应用程序设置为使用常规路由,当您首次创建应用程序时,它应该随模板一起提供:
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?}");
});
}
}
}
您使用 [Route]
属性的方式给我的印象是您不知道它们是什么,或者至少您很困惑。使用常规路由,即使您不在控制器上放置 [Route]
,以下请求也应该通过“默认”路由到达其相应的控制器操作:
/*
* /moviesList/index GET => MoviesList controller, Index action
* /moviesList/search GET => MoviesList controller, Search action
*/
顺便说一下,一个名为 MoviesListController
的控制器很糟糕。我就叫它 MovieController
.
3。表格 Post
在表单中,您不能指定控制器和提交按钮上的操作。反正也不是锚标签。
而<input type="hidden" name="ID" value="@ViewBag.pageID"
在表格之外。表单如何知道那是什么并 post 返回正确的值?
4。过度使用 ViewBag / ViewData
从技术上讲,您只能使用 ViewBag
在控制器之间传输数据以查看。 ViewData
只在当前请求有效,只能从controller传数据给view,不能vice-versa.
此外,它们是 so-called 弱类型 collections。它们旨在将少量数据传入和传出控制器和视图,例如页面标题。如果过度使用它们,您的应用程序将变得难以维护,因为您在使用时必须记住数据的类型。
通过过度使用 ViewBag / ViewData
,您基本上删除了 C# 和 Razor 的最佳功能之一 - 强类型。
最好的方法是在视图中指定一个视图模型。您将视图模型的实例从控制器操作传递到视图。视图模型只定义视图需要的数据!您不应该将整个数据库模型传递给视图,以便用户可以使用您的其他重要信息!
我的做法
我不想使用单一方法来处理列出所有电影和搜索过滤器,而是想将它们分开。搜索表单将使用 [HttpPost]
而不是 [HttpGet]
.
这样我只需要 post 返回搜索过滤器数据,我现在可以在 Index 操作上定义自定义参数并将 Post 操作重定向到 Index 操作。
我会告诉你我的意思。
查看型号
首先,我将定义视图所需的所有视图模型:
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; }
}
}
控制器
接下来是控制器:
这里要注意的一件事是,您必须以与在视图模型中定义它的方式相同的方式命名 POST 操作参数,例如 MovieListSearchViewModel search
.
您不能将参数名称命名为其他名称,因为我们正在 post 将部分视图模型返回 MVC,默认情况下,模型绑定只会为您绑定数据,如果它匹配名字.
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
}
}
景色
终于来了视图:
@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>
截图
当您第一次登陆“电影”页面时:
可用流派列表以及电影列表已正确显示:
按流派搜索:
按标题搜索:
我看过许多教程和文档将模型作为参数传递到控制器的 Action 中。每次执行此操作时,都会在调用操作时收到 415 状态错误(媒体类型不正确)。这对我来说是有问题的,因为我的字段在操作发生后清除。许多人建议在我 return 视图时调用模型,但这对我不起作用。有谁知道这是为什么以及我该如何解决?我很沮丧,我尝试了很多东西,但都没有用:(
我想如何将模型作为参数传递的示例:
[HttpGet("[action]")]
public async Task<IActionResult> Search(Movies model, int ID, string titleSearch,
string genreSearch)
{
return View(model);
}
我的看法:
@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>
我的控制器:
//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());
}
总的来说,我的目标是在我的操作完成后不清除任何字段,并允许用户使用新条目重新调用操作。据我了解,将模型作为参数传递可以帮助我实现我的目标,但我没有任何运气。请让我知道如何实现这个目标。感谢您的宝贵时间!
您实际上并没有向控制器操作“传递参数”——您是在向应用程序定义的端点发出 HTTP 请求,应用程序中的各种中间件 运行 会尝试处理该端点。在这种情况下,其中一个中间件是 MVC framework/module,它试图将路由值(控制器、操作等)映射到匹配的 classes,并在相关时查询字符串或表单值。
由于您已将该搜索操作定义为仅匹配 GET 请求,因此您正在从查询字符串(您通常在导航栏中看到的 ?foo=bar&bar=baz
内容)中读取。 C# class 不是您可以作为查询字符串值发送的东西(有一些方法可以解决这个问题,使用属性,但这对您的示例来说有点矫枉过正)。如果您还没有阅读,我会阅读 https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1。
上一个示例中的搜索操作将起作用,但您已将输入呈现在 <form>
元素之外;要包含它,您需要在表单内呈现它或使用 form="form id here"
属性将其与该表单相关联(您需要向表单添加一个 id="something"
属性,以便工作,以及)。
<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>
如果您想保留用于提交搜索表单的值,您有两个选择(好吧,实际上更多,但我们现在假设两个):
- 将查询字符串值添加到 ViewBag/ViewData(你开始这样做了)
- 使用实际的视图模型,而不是值的集合
我个人会选择#2,因为它还可以使您的视图更易于绑定。所以:
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; }
}
查看:
@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>
控制器
如果您将操作参数设置为可为空,则实际上“默认”操作和搜索都只需要一个操作:
[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);
}
这完全未经测试,所以买者自负。
您的代码中有很多错误。我不确定从哪里开始,但会尽力列出一些:
- 使用
[HttpGet]
- 属性路由的使用,
[Route]
- 表格post
- 过度使用
ViewBag
1。使用 [HttpGet]
我不想说你使用 [HttpGet]
传递名称作为参数的方式是错误的,但你的设置将始终忽略控制器名称!
你传入的[action]
是call token replacement,会替换成action name的值所以:
/*
* [HttpGet("[action]")] on Search action => [HttpGet("search")] => matches /search
* [HttpGet("[action]")] on Index action => [HttpGet("index")] => matches /index
*/
看看这是多么错误!您缺少控制器名称!
请求 /moviesList/index
不会从 MoviesList 控制器调用 Index 方法,但请求 /index
会!
把template/token替换参数去掉就行了。默认情况下,如果您不使用任何 HTTP 动词模板标记控制器操作,即 [HttpGet]
,它们将默认处理 HTTP GET 请求。
2。使用属性路由,[Route]
我不想说在 Model-View-Controller 应用程序中使用属性路由是错误的,但属性路由主要用于构建 RESTful API 应用程序。
默认情况下,应用程序设置为使用常规路由,当您首次创建应用程序时,它应该随模板一起提供:
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?}");
});
}
}
}
您使用 [Route]
属性的方式给我的印象是您不知道它们是什么,或者至少您很困惑。使用常规路由,即使您不在控制器上放置 [Route]
,以下请求也应该通过“默认”路由到达其相应的控制器操作:
/*
* /moviesList/index GET => MoviesList controller, Index action
* /moviesList/search GET => MoviesList controller, Search action
*/
顺便说一下,一个名为 MoviesListController
的控制器很糟糕。我就叫它 MovieController
.
3。表格 Post
在表单中,您不能指定控制器和提交按钮上的操作。反正也不是锚标签。
而<input type="hidden" name="ID" value="@ViewBag.pageID"
在表格之外。表单如何知道那是什么并 post 返回正确的值?
4。过度使用 ViewBag / ViewData
从技术上讲,您只能使用 ViewBag
在控制器之间传输数据以查看。 ViewData
只在当前请求有效,只能从controller传数据给view,不能vice-versa.
此外,它们是 so-called 弱类型 collections。它们旨在将少量数据传入和传出控制器和视图,例如页面标题。如果过度使用它们,您的应用程序将变得难以维护,因为您在使用时必须记住数据的类型。
通过过度使用 ViewBag / ViewData
,您基本上删除了 C# 和 Razor 的最佳功能之一 - 强类型。
最好的方法是在视图中指定一个视图模型。您将视图模型的实例从控制器操作传递到视图。视图模型只定义视图需要的数据!您不应该将整个数据库模型传递给视图,以便用户可以使用您的其他重要信息!
我的做法
我不想使用单一方法来处理列出所有电影和搜索过滤器,而是想将它们分开。搜索表单将使用 [HttpPost]
而不是 [HttpGet]
.
这样我只需要 post 返回搜索过滤器数据,我现在可以在 Index 操作上定义自定义参数并将 Post 操作重定向到 Index 操作。
我会告诉你我的意思。
查看型号
首先,我将定义视图所需的所有视图模型:
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; }
}
}
控制器
接下来是控制器:
这里要注意的一件事是,您必须以与在视图模型中定义它的方式相同的方式命名 POST 操作参数,例如 MovieListSearchViewModel search
.
您不能将参数名称命名为其他名称,因为我们正在 post 将部分视图模型返回 MVC,默认情况下,模型绑定只会为您绑定数据,如果它匹配名字.
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
}
}
景色
终于来了视图:
@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>
截图
当您第一次登陆“电影”页面时:
可用流派列表以及电影列表已正确显示:
按流派搜索:
按标题搜索: