簡體   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

我已經看到許多教程和文檔通過 model 作為 controller 中的操作中的參數。 每次執行此操作時,我都會在調用操作時收到 415 狀態錯誤(不正確的媒體類型)。 這對我來說是有問題的,因為在操作發生后我的字段會清除。 許多人建議在我返回視圖時調用 model,但這對我不起作用。 有誰知道這是為什么以及我該如何解決? 我很沮喪,我嘗試了很多東西,但它從來沒有奏效:(

我想如何將 model 作為參數傳遞的示例:

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

我的 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());
}

總的來說,我的目標是在我的操作完成后不清除任何字段,並允許用戶使用新條目重新調用操作。 據我了解,將 model 作為參數傳遞可以幫助我實現目標,但我沒有任何運氣。 請讓我知道如何實現這一目標。 感謝您的時間!

您的代碼中有很多錯誤。 我不知道從哪里開始,但會盡力列出一些:

  1. [HttpGet]的使用
  2. 使用屬性路由, [Route]
  3. 表格帖子
  4. 過度使用ViewBag

1. [HttpGet]的使用

我不想說您使用[HttpGet]傳遞名稱作為參數的方式是錯誤的,但是您的設置將始終忽略 controller 名稱!

您傳入的[action]是調用令牌替換,它將被替換為操作名稱的值,因此:

/*
 * [HttpGet("[action]")] on Search action  =>  [HttpGet("search")]  =>  matches /search
 * [HttpGet("[action]")] on Index action   =>  [HttpGet("index")]   =>  matches /index
 */

看看這是多么錯誤! 您缺少 controller 名稱!

請求/moviesList/index不會調用 MoviesList controller 中的 Index 方法,但請求/index會!

只需取出模板/令牌替換參數即可。 默認情況下,如果您不使用任何 HTTP 動詞模板(即[HttpGet] )標記 controller 操作,則它們默認處理 HTTP 請求。


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]放在控制器上,以下請求也應通過“默認”路由到達其相應的 controller 操作:

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

順便說一句,名為MoviesListController的 controller 很糟糕。 我將稱之為MovieController


3.表格發布

在表單中,您不能指定 controller 和提交按鈕上的操作。 無論如何,它不是錨標簽。

並且<input type="hidden" name="ID" value="@ViewBag.pageID"在表單之外。 表單如何知道那是什么並返回正確的值?


4. ViewBag / ViewData的過度使用

從技術上講,您只能使用ViewBag在 controller 之間傳輸數據以查看。 ViewData只在當前請求中有效,只能從controller傳輸數據到view,反之不行。

此外,它們是所謂的弱類型 collections。 它們旨在將少量數據傳入和傳出控制器和視圖,例如頁面標題。 如果您過度使用它們,您的應用程序將變得非常難以維護,因為您必須記住使用數據時的數據類型。

通過過度使用ViewBag / ViewData ,您基本上刪除了關於 C# 和 Razor 的最佳功能之一 - 強類型。

最好的方法是在視圖中指定一個視圖 model。 您將視圖 model 的實例傳遞給 controller 操作的視圖。 視圖 model 只定義了視圖需要的數據! 您不應將整個數據庫 model 傳遞給視圖,以便用戶可以使用您的其他重要信息!



我的方法

我不想使用單一方法來處理列出所有電影以及搜索過濾器,而是想將它們分開。 搜索表單將使用[HttpPost]而不是[HttpGet]

這樣我只需要回發搜索過濾器數據,我現在可以在 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; }
    }
}

Controller

接下來是controller:

這里要注意的一件事是,您必須以與在視圖 model 中定義它的方式相同的方式命名 POST 操作參數,例如MovieListSearchViewModel search

您不能將參數名稱命名為其他名稱,因為我們將部分視圖 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
    }
}

風景

最后是視圖:

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

截圖

當您第一次登陸電影頁面時:

在此處輸入圖像描述

可用的流派列表以及電影列表正確顯示:

在此處輸入圖像描述

按類型搜索:

在此處輸入圖像描述

按標題搜索:

在此處輸入圖像描述

您並沒有真正向 controller 操作“傳遞參數” - 您正在向應用程序定義的端點發出 HTTP 請求,在您的應用程序中運行的各種中間件試圖處理這些請求。 在這種情況下,這些中間件之一是 MVC 框架/模塊,它嘗試將 map 路由值(控制器、操作等)到匹配的類,並在相關的地方查詢字符串或表單值。

由於您已將 Search 操作定義為僅匹配 GET 請求,因此您正在讀取查詢字符串(您通常在導航欄中看到的?foo=bar&bar=baz內容)。 A C# class 不是您可以作為查詢字符串值發送的東西(有一些方法可以解決這個問題,使用屬性,但這對於您的示例來說有點矯枉過正)。 如果你還沒有讀過,我會讀https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1

上一個示例中的 Search 操作將起作用,但您已將輸入呈現在<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. 使用實際視圖 model,而不是值的集合

我個人將 go 與 #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>

Controller

如果你讓你的動作參數可以為空,你實際上只需要一個動作用於“默認”動作和搜索:

[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);
}

這是完全未經測試的,所以請謹慎購買。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM