简体   繁体   English

将 Model 传递到 Controller 时的 415 状态 ASP.NET Core 3.1 MVC 中的操作

[英]415 Status When Passing Model into Controller Action in ASP.NET Core 3.1 MVC

I've seen many tutorials and documentation pass the model as an argument in an Action in a controller.我已经看到许多教程和文档通过 model 作为 controller 中的操作中的参数。 Every time I do this I get a 415 status error (incorrect media type) when the action is called.每次执行此操作时,我都会在调用操作时收到 415 状态错误(不正确的媒体类型)。 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.许多人建议在我返回视图时调用 model,但这对我不起作用。 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:我想如何将 model 作为参数传递的示例:

[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:我的 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.据我了解,将 model 作为参数传递可以帮助我实现目标,但我没有任何运气。 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:我不知道从哪里开始,但会尽力列出一些:

  1. Use of [HttpGet] [HttpGet]的使用
  2. Use of Attribute Routing, [Route]使用属性路由, [Route]
  3. Form post表格帖子
  4. Overuse of ViewBag过度使用ViewBag

1. Use of [HttpGet] 1. [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!我不想说您使用[HttpGet]传递名称作为参数的方式是错误的,但是您的设置将始终忽略 controller 名称!

The [action] you passed in is call token replacement, which will be replaced with the value of the action name so:您传入的[action]是调用令牌替换,它将被替换为操作名称的值,因此:

/*
 * [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!您缺少 controller 名称!

A request /moviesList/index will not call the Index method from the MoviesList controller, but a request /index will!请求/moviesList/index不会调用 MoviesList controller 中的 Index 方法,但请求/index会!

Just take out the template/token replacement parameter.只需取出模板/令牌替换参数即可。 And by default, if you don't mark the controller action with any HTTP verb templates, ie, [HttpGet] , they're default to handle HTTP GET requests.默认情况下,如果您不使用任何 HTTP 动词模板(即[HttpGet] )标记 controller 操作,则它们默认处理 HTTP 请求。


2. Use of Attribute Routing, [Route] 2. 属性路由的使用, [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.我不想说在 Model-View-Controller 应用程序中使用属性路由是错误的,但属性路由主要用于构建 RESTful API 应用程序时。

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.您使用[Route]属性的方式给我的印象是您不知道它们是什么,或者至少您很困惑。 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:使用常规路由,即使您不将[Route]放在控制器上,以下请求也应通过“默认”路由到达其相应的 controller 操作:

/*
 * /moviesList/index     GET    =>    MoviesList controller, Index action
 * /moviesList/search    GET    =>    MoviesList controller, Search action
 */

By the way, a controller named MoviesListController is awful.顺便说一句,名为MoviesListController的 controller 很糟糕。 I will just call it MovieController .我将称之为MovieController


3. Form Post 3.表格发布

Within the form, you can't specify a controller and the action on the submit button.在表单中,您不能指定 controller 和提交按钮上的操作。 It's not an anchor tag anyway.无论如何,它不是锚标签。

And <input type="hidden" name="ID" value="@ViewBag.pageID" is outside the form.并且<input type="hidden" name="ID" value="@ViewBag.pageID"在表单之外。 How would the form know what that is and post the correct value back?表单如何知道那是什么并返回正确的值?


4. Overuse of ViewBag / ViewData 4. ViewBag / ViewData的过度使用

Technically you can only use ViewBag to transfer data between controller to view.从技术上讲,您只能使用ViewBag在 controller 之间传输数据以查看。 ViewData is only valid in the current request, and you can only transfer data from controller to view, not vice-versa. ViewData只在当前请求中有效,只能从controller传输数据到view,反之不行。

In additional, they're so-called weakly typed collections.此外,它们是所谓的弱类型 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.通过过度使用ViewBag / ViewData ,您基本上删除了关于 C# 和 Razor 的最佳功能之一 - 强类型。

The best approach is to specify a view model in the view.最好的方法是在视图中指定一个视图 model。 You pass an instance of the view model to the view from the controller action.您将视图 model 的实例传递给 controller 操作的视图。 The view model defines only the data the view needs!视图 model 只定义了视图需要的数据! You should not pass your entire database model to the view so that users can use your other important information!您不应将整个数据库 model 传递给视图,以便用户可以使用您的其他重要信息!



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] .搜索表单将使用[HttpPost]而不是[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.这样我只需要回发搜索过滤器数据,我现在可以在 Index 操作上定义自定义参数,并将 Post 操作重定向到 Index 操作。

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 Controller

Next, here comes the controller:接下来是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 .这里要注意的一件事是,您必须以与在视图 model 中定义它的方式相同的方式命名 POST 操作参数,例如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.您不能将参数名称命名为其他名称,因为我们将部分视图 model 发布回 MVC,默认情况下,model 绑定只会在与名称匹配时为您绑定数据。

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.您并没有真正向 controller 操作“传递参数” - 您正在向应用程序定义的端点发出 HTTP 请求,在您的应用程序中运行的各种中间件试图处理这些请求。 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.在这种情况下,这些中间件之一是 MVC 框架/模块,它尝试将 map 路由值(控制器、操作等)到匹配的类,并在相关的地方查询字符串或表单值。

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).由于您已将 Search 操作定义为仅匹配 GET 请求,因此您正在读取查询字符串(您通常在导航栏中看到的?foo=bar&bar=baz内容)。 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). A C# class 不是您可以作为查询字符串值发送的东西(有一些方法可以解决这个问题,使用属性,但这对于您的示例来说有点矫枉过正)。 I'd read https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1 if you haven't already.如果你还没有读过,我会读https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1

The Search action in your last example will work, but you've rendered the input outside the <form> element;上一个示例中的 Search 操作将起作用,但您已将输入呈现在<form>元素之外; 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="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>

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:如果您想保留用于提交搜索表单的值,您有两个选择(嗯,更实际,但现在说两个):

  1. Add the querystring values to ViewBag/ViewData (you started to do this)将查询字符串值添加到 ViewBag/ViewData(您开始这样做)
  2. Use an actual view model, rather than a collection of values使用实际视图 model,而不是值的集合

I'd personally go with #2, since it also makes your view cleaner to bind.我个人将 go 与 #2 一起使用,因为它还可以使您的视图更清晰地绑定。 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 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.这是完全未经测试的,所以请谨慎购买。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何在 ASP.NET Core 3.1 中为 HTTP 状态 415 创建自定义响应? - How to create a custom response for HTTP status 415 in ASP.NET Core 3.1? ASP.NET 内核 3.1 | 参数 model 总是 null 与 controller 动作方法中的大表格数据 - ASP.NET Core 3.1 | Argument model always null with large form data in controller action method Asp.net Core MVC-在控制器动作中的窗体上发布到控制器IEnumerable模型为空 - Asp.net Core MVC - on form posting to controller IEnumerable model in controller action is empty 将两个或多个参数从 model 视图传递到 EF Core / ASP.NET Core MVC 应用程序中的 controller - Passing two or more parameters from model view to controller in EF Core / ASP.NET Core MVC application Asp.Net Core MVC - 复杂的 Model 未绑定到 Get Controller 操作 - Asp.Net Core MVC - Complex Model not binding on Get Controller action ASP.NET 核心 MVC - 将 Model 数据从视图传递回 Controller - ASP.NET Core MVC - Passing Model Data Back to Controller from View 将复杂的 model 传递给 controller 使用 Z9E0DA8438E1E38A1C30F4B76CE3 Core M34A6659BCEAE779F28185E757ABFCA5Z - Passing a complex model to controller using AJAX in ASP.NET Core MVC 将接口传递给ASP.NET MVC Controller Action方法 - Passing an interface to an ASP.NET MVC Controller Action method ASP.NET MVC CORE 中的控制器动作别名 - Controller action alias in ASP.NET MVC CORE 将模型从一个动作传递到另一个asp.net mvc - Passing model from one action to another asp.net mvc
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM