Razor Page入門 - 3

by vivid 8. 七月 2020 04:52

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N200722001
出刊日期: 2020/7/8

這篇文章將延續《Razor Page入門 - 2》一文的情境,介紹如何在ASP.NET Core Razor Page網站應用程式之中,利用路由與查詢字串傳遞參數資料到其它Razor Page網頁,根據路由或查詢字串的值,來設計資料查詢網頁。

ASP.NET Core Razor Page網站應用程式內建許多設計慣例,以及組態設定,來控制路由(Route)系統的運作,路由(Route)底層的基礎架構和ASP.NET Core MVC網站應用程式共用,而兩者之間的差別是在路由的組態設定不太一樣。在ASP.NET Core MVC網站應用程式中,採用Attribute路由(Attribute Routing);而ASP.NET Core Razor Page網站應用程式則是根據對應到URL的Razor檔案之檔名來決定要執行的Razor Page網頁。

ASP.NET Core Razor Page網頁是存放在「Pages」資料夾中,預設文件的檔名為「Index.cshtml」,例如以下的URL網址:

https://localhost:44300/Index

與以下的URL網址

https://localhost:44300/

將會執行網站專案中「Pages/Index.cshtml」檔案;而以下的URL網址:

https://localhost:44300/Books

與以下的URL網址:

https://localhost:44300/Books/Index

將會執行網站專案「Pages/Books/Index.cshtml」檔案;而以下的URL網址:

https://localhost:44300/Books/List

將會執行網站專案「Pages/Books/List.cshtml」檔案。

若我們的「MyRazorWeb」網站專案檔案結構如下圖,在「Pages」資料夾中包含一個「Book.cshtml」檔案;同時在「Pages/Books」資料夾中包含一個「Index.cshtml」檔案:

clip_image002

圖 1:網站專案檔案結構。

當執行網站時,以下的URL:

https://localhost:44300/Books

將會造成「AmbiguousMatchException」例外錯誤,訊息如下:

AmbiguousMatchException: The request matched multiple endpoints. Matches: /Books /Books/Index

執行結果,請參考下圖所示:

clip_image004

圖 2:「AmbiguousMatchException」例外錯誤。

這是因為這個URL網址對應到:

https://localhost:44300/Books.cshtml

https://localhost:44300/Books/Index.cshtml

這兩個Razor Page網頁。要解決這個例外錯誤最簡單的方式就是將Razor Page網頁改名,否則就需要利用路由參數來覆寫預設路由。

 

覆寫預設路由

舉例來說,修改「Pages\Books\Index.cshtml」檔案中的程式碼,在第一行程式使用「@page」指示詞,指明路由位置為「BookIndex」,請參考以下程式碼:

MyRazorWeb\Pages\Books\Index.cshtml

@page "BookIndex"
@model MyRazorWeb.Pages.Books.IndexModel
@{
    ViewData["Title"] = "Index";
}

<h1> Books Index </h1>


 

那麼以下的URL:

https://localhost:44300/Books/BookIndex

就可以存取到「Pages\Books\Index.cshtml」,請參考下圖所示:

clip_image006

圖 3:覆寫預設路由。

而以下的URL:

https://localhost:44300/Books

便可以存取到「Pages\Books.cshtml」,請參考下圖所示:

clip_image008

圖 4:覆寫預設路由。

若我們想要傳遞路由參數「title」到「Pages\Books\Index.cshtml」,可以修改「Pages\Books\Index.cshtml」檔案中的程式碼,在第一行程式使用「@page」指示詞,指明路由位置為「BookIndex/{title}」,「{title}」是自訂的參數,在Razor Page中便可以使用「RouteData.Values["title"]」來讀取「{title}」參數,請參考以下「Index.cshtml」程式碼:

MyRazorWeb\Pages\Books\Index.cshtml

@page "BookIndex/{title}"
@model MyRazorWeb.Pages.Books.IndexModel
@{
    ViewData["Title"] = "Index";
}

<h1> Books Index </h1>

@RouteData.Values["title"]

我們以以下URL網址來執行「Pages\Books\Index.cshtml」:

https://localhost:44300/Books/BookIndex/Marvel

執行的結果請參考下圖所示:

clip_image010

圖 5:讀取路由參數。

篩選資料

接下來要回到我們的「MyRazorWeb」圖書資料的設計,我們希望能夠能有一個「Details」Razor Page,讓使用者查詢特定圖書資料的詳細資訊。先修改「MyServices」服務專案「IBookRepository」介面程式碼,新增一個「GetBook」方法,根據「id」參數的值,來查詢「id」相符的圖書資料:

MyServices\IBookRepository.cs

using MyModels;
using System;
using System.Collections.Generic;
using System.Text;

namespace MyServices {
  public interface IBookRepository {
    IEnumerable<Book> GetAllBooks();
    Book GetBook( int id );
  }
}


接著我們在「BookRepository」類別中,實作「GetBook」方法,請參考以下程式碼:

MyServices\BookRepository.cs

using MyModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyServices {
  public class BookRepository : IBookRepository {
    private List<Book> _books = null;
    public BookRepository() {
      _books = new List<Book>() {
         new Book() {
           Id = 1 ,
           Title = " Essential Programming Language " ,
           Price = 250 ,
           PublishDate = new DateTime( 2019 ,1,2 ) ,
           InStock = true ,
           Description = "Essential Programming Language "  ,
          Category = Category.Computers
         },
         new Book() {
           Id = 2 ,
           Title = " Telling Arts " ,
           Price = 245 ,
           PublishDate = new DateTime( 2019 , 4 , 15 ) ,
           InStock = true ,
           Description = " Telling Arts "  ,
          Category = Category.Arts
         },
           new Book() {
           Id = 3 ,
           Title = " Marvel " ,
           Price = 150  ,
           PublishDate = new DateTime( 2019 , 2, 21 ) ,
           InStock = true ,
           Description = " Marvel "  ,
          Category = Category.Commics
         },
          new Book() {
           Id = 4 ,
           Title = " The Beauty of Cook" ,
           Price = 450 ,
           PublishDate = new DateTime( 2019 ,12,2 ) ,
           InStock = true ,
           Description = " The Beauty of Cook "  ,
           Category = Category.Cooking
         }
      };
    }
    public IEnumerable<Book> GetAllBooks() {
      return _books;
    }

    public Book GetBook( int id ) {
      return _books.FirstOrDefault( b => b.Id == id );
    }
  }
}

 

使用查詢字串(Query String)

在「MyRazorWeb」專案,我們想要為圖書系統加上功能,讓使用者可以在「List」Razor Page網頁,選取並查詢特定圖書的詳細資料,「List」Razor Page將利用查詢字串(Query String)傳遞圖書代號(id)到「Details」Razor Page網頁,在網頁中將此筆圖書資料取回並顯示在「Details」Razor Page網頁之中。

修改「Pages\Books\List.cshtml」檔案,在每一筆圖書資料後方加上一個<a>超連結,以導向「Details」Razor Page,參考以下程式碼:

MyRazorWeb\Pages\Books\List.cshtml

@page
@model MyRazorWeb.ListModel
@{
    ViewData["Title"] = "List";
}

<h1>List</h1>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Id )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Title )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Price )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].PublishDate )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].InStock )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Description )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Category )
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach ( var item in Model.Books ) {
        <tr>
            <td>
                @Html.DisplayFor( modelItem => item.Id )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Title )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Price )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.PublishDate )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.InStock )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Description )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Category )
            </td>
            <td>
                <a asp-page="./Details" asp-route-id="@item.Id"> Details </a> |
            </td>
        </tr>
        }
    </tbody>
</table>

「asp-page」標記協助程式(Tag Helper)指明當你點選超連結時,會導向「Details」Razor Page。「asp-route-id」標記協助程式(Tag Helper)則會將「Book」物件「id」屬性值傳遞到「Details」Razor Page。

在「Solution Explorer」視窗,「MyRazorPage」專案 - 「Pages \ Books」資料夾項目上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Razor Page」項目,請參考下圖所示:

clip_image012

圖 6:加入「Razor Page」項目。

在「Add New Scaffolded Item」對話盒左方選取「Razor Pages」分類,然後選取「Razor Page」項目,按下「Add 」按鈕,請參考下圖所示:

clip_image014

圖 7:選取「Razor Page」項目。

在「Add Razor Page」對話盒中,設定Razor Page名稱為「Details」,勾選下方「Generate PageModel class」以及「Use a layout page」項目;然後按下「Add 」按鈕,請參考下圖所示:

clip_image016

圖 8:「Add Razor Page」對話盒。

在Visual Studio工具產生的「Details」Razor Page類別檔案加入以下程式碼:

MyRazorWeb\Pages\Books\Details.cshtml.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyModels;
using MyServices;

namespace MyRazorWeb.Pages.Books {
  public class DetailsModel : PageModel {
    private readonly IBookRepository bookRepository;
    public DetailsModel( IBookRepository bookRepository ) {
      this.bookRepository = bookRepository;
    }
    public Book Book { get; private  set; }
    public void OnGet(int id) {
      Book = bookRepository.GetBook( id );
    }
  }
}

 

預設ASP.NET Core 模型繫結(Model -Binding)功能,會自動將搜集到的「id」查詢字串的值,對應到「DetailsModel」類別「OnGet」方法的「id」參數之中。

然後在「Details」Razor Page 檔案加入以下程式碼,利用HTML協助程式(HTML Helper)的「DisplayNameFor() 」方法顯示「Book」模型類別屬性名稱 (Display Attribute);利用「DisplayFor()」方法顯示模型類別屬性值;然後利用Anchor標記協助程式(Anchor Tag Helper)來產生一個回到「List」Razor Page的超連結:

MyRazorWeb\Pages\Books\Details.cshtml

@page
@model MyRazorWeb.Pages.Books.DetailsModel
@{
    ViewData["Title"] = "Details";
}
<div>
    <h1>Book Details</h1>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor( model => model.Book.Title )
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor( model => model.Book.Title )
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor( model => model.Book.Price )
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor( model => model.Book.Price )
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor( model => model.Book.PublishDate )
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor( model => model.Book.PublishDate )
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor( model => model.Book.InStock )
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor( model => model.Book.InStock )
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor( model => model.Book.Description )
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor( model => model.Book.Description )
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor( model => model.Book.Category )
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor( model => model.Book.Category )
        </dd>
    </dl>
</div>
<div>
    @*<a asp-page="./Edit" asp-route-id="@Model.Book.Id">Edit</a> |*@
    <a asp-page="./List">Back to List</a>
</div>

 

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站「Books/List」Razor Page,執行結果參考如下:

clip_image018

圖 9:「Books/List」Razor Page測試。

當你點選某筆圖書後方的「Details」超連結時,「Book」物件的「id」屬性值就以查詢字串的型式,傳遞到「Details」Razor Page:

https://localhost:44300/books/details/?id=1

如此我們便可利用查詢字串在「Books/Details」Razor Page篩選與顯示圖書資料,執行結果,請參考下圖所示:

clip_image020

圖 10:利用查詢字串篩選與顯示圖書資料。

客製化路由 – LowercaseUrls屬性

ASP.NET Core內建一個「RouteOptions」物件,可以用來客製化路由的查詢字串參數,例如我們可以設定「RouteOptions」物件「LowercaseUrls」屬性,當它的值為「true」時,URL中的文字會被全部轉換成英文的小寫;若「LowercaseUrls」屬性為「false」時,URL中的文字則會根據設計階段的英文大小寫來做顯示。舉例來說,修改「MyRazorWeb」專案的「Startup.cs」的「ConfigureServices」方法,加上以下程式碼:

MyRazorWeb\Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyServices;

namespace MyRazorWeb {
  public class Startup {
    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services ) {
      services.AddSingleton<IBookRepository , BookRepository>();
      services.Configure<RouteOptions>( options => {
        options.LowercaseUrls = true;
      } );
      services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      else {
        app.UseExceptionHandler( "/Error" );
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints( endpoints => {
        endpoints.MapRazorPages();
      } );
    }
  }
}

則執行Razor Page網頁時,URL上的文字就會自動轉成英文小寫:

https://localhost:44300/books/list

「Books\List.cshtml」檔案執行結果,請參考下圖所示:clip_image022

圖 11:「RouteOptions」物件「LowercaseUrls」屬性值為「true」。

「Books\Details.cshtml」檔案執行後的URL為:

https://localhost:44300/books/details?id=1

執行結果,請參考下圖所示:

clip_image024

圖 12:「RouteOptions」物件「LowercaseUrls」屬性值為「true」。

LowercaseQueryStrings屬性

「RouteOptions」物件「LowercaseUrls」屬性值為「true」時,URL中查詢字串的文字會被全部轉換成英文的小寫;但有一個先決條件,「LowercaseUrls」屬性值為要設為「true」時,「LowercaseQueryStrings」屬性值設為「true」才會有效果。讓我們修改一下「MyRazorWeb\Pages\Books\List.cshtml」檔案的程式碼,故意將「asp-route-Id」標記協助程式(Tag Helper)中「Id」的「I」設為英文的大寫:

<a asp-page="./Details" asp-route-Id="@item.Id"> Details </a> |

目前「MyRazorWeb\Pages\Books\List.cshtml」檔案的程式碼看起來如下:

MyRazorWeb\Pages\Books\List.cshtml

@page
@model MyRazorWeb.ListModel
@{
    ViewData["Title"] = "List";
}

<h1>List</h1>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Id )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Title )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Price )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].PublishDate )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].InStock )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Description )
            </th>
            <th>
                @Html.DisplayNameFor( model => ( (IList<MyModels.Book>) model.Books )[0].Category )
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach ( var item in Model.Books ) {
        <tr>
            <td>
                @Html.DisplayFor( modelItem => item.Id )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Title )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Price )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.PublishDate )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.InStock )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Description )
            </td>
            <td>
                @Html.DisplayFor( modelItem => item.Category )
            </td>
            <td>
                <a asp-page="./Details" asp-route-Id="@item.Id"> Details </a> |
            </td>
        </tr>
        }
    </tbody>
</table>

 

測試「Details」Razor Page網頁,參考網址,目前查詢字串中「Id」的「I」字仍是大寫:

https://localhost:44300/books/details?Id=1

執行結果請參考下圖所示:

clip_image026

圖 13:「RouteOptions」物件「LowercaseUrls」屬性值為「false」。

修改「MyRazorWeb」專案的「Startup.cs」的「ConfigureServices」方法,加上以下程式碼,將「RouteOptions」物件的「LowercaseQueryStrings」屬性的值設為「true」:

MyRazorWeb\Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyServices;

namespace MyRazorWeb {
  public class Startup {
    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services ) {
      services.AddSingleton<IBookRepository , BookRepository>();
      services.Configure<RouteOptions>( options => {
        options.LowercaseUrls = true;
        options.LowercaseQueryStrings = true;
      } );
      services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      else {
        app.UseExceptionHandler( "/Error" );
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints( endpoints => {
        endpoints.MapRazorPages();
      } );
    }
  }
}

再度測試「Details」Razor Page,這次查詢字串中「id」的「i」字已轉換成英文小寫:

https://localhost:44300/books/details?id=1

執行結果請參考下圖所示:

clip_image028

圖 14:「RouteOptions」物件「LowercaseUrls」屬性值為「true」。

AppendTrailingSlash屬性

「RouteOptions」物件「AppendTrailingSlash」屬性值為「true」時,會自動在URL最後附加一個「/」符號,例如修改「MyRazorWeb\Startup.cs」檔案程式碼如下:

MyRazorWeb\Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyServices;

namespace MyRazorWeb {
  public class Startup {
    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services ) {
      services.AddSingleton<IBookRepository , BookRepository>();
      services.Configure<RouteOptions>( options => {
        options.LowercaseUrls = true;
        options.LowercaseQueryStrings = true;
        options.AppendTrailingSlash = true;
      } );
      services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      else {
        app.UseExceptionHandler( "/Error" );
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints( endpoints => {
        endpoints.MapRazorPages();
      } );
    }
  }
}

 

測試「List或「Details」網頁,檢視URL最後附加一個「/」符號:

https://localhost:44300/books/list/

執行結果,請參考下圖所示:

clip_image030

圖 15:「RouteOptions」物件「AppendTrailingSlash」屬性值為「true」。

Tags:

.NET Magazine國際中文電子雜誌 | ASP.NET Razor Pages | Visual Studio | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List