在 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>

如果您想保留用于提交搜索表单的值,您有两个选择(好吧,实际上更多,但我们现在假设两个):

  1. 将查询字符串值添加到 ViewBag/ViewData(你开始这样做了)
  2. 使用实际的视图模型,而不是值的集合

我个人会选择#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);
}

这完全未经测试,所以买者自负。

您的代码中有很多错误。我不确定从哪里开始,但会尽力列出一些:

  1. 使用[HttpGet]
  2. 属性路由的使用,[Route]
  3. 表格post
  4. 过度使用 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>

截图

当您第一次登陆“电影”页面时:

可用流派列表以及电影列表已正确显示:

按流派搜索:

按标题搜索: