【翻译】介绍 ASP.NET Core 中的 Razor Pages

时间:2017-09-22 00:11:49   收藏:0   阅读:1259

介绍 ASP.NET Core 中的 Razor Pages

原文地址:Introduction to Razor Pages in ASP.NET Core         译文地址:介绍 asp.net core 中的 Razor Pages           翻译:ganqiyin

Razor Pages 是 ASP.NET Core MVC 的一个新功能,可以让基于页面的编程方案更容易,更高效。

如果您正在寻找使用 Model-View-Controller 的教程,请参阅ASP.NET Core MVC入门。

使用 ASP.NET Core 2.0 的前提条件

安装 .NET Core 2.0.0 或更高版本.

如果您使用 Visual Studio 工具, 请安装具有以下工作组件的 Visual Studio 15.3 或更高版本:

创建一个Razor Pages项目

Visual Studio

有关如何使用Visual Studio创建 Razor Pages 项目的详细说明,请参阅“ Razor Pages入门”。

Visual Studio for Mac

从命令行运行 dotnet new razor 。

从 Visual Studio for Mac 打开生成的 .csproj 文件。

Visual Studio Code

从命令行运行 dotnet new razor 。

.NET Core CLI

从命令行运行 dotnet new razor 。


Razor Pages

Razor Pages 在 Startup.cs 启用:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Includes support for Razor Pages and controllers.
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc();
    }
}

 

研究一个基础页面:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

  

前面的代码看起来很像Razor视图文件。 @page 指令使它与众不同. @page 让文件变成一个 MVC action - 这意味着它可以直接处理请求,而无需通过控制器。 @page 必须是页面上的第一个Razor指令。 @page 会影响其他Razor结构的行为。 @functions 指令启用 function-level 内容。

以下两个文件中显示了一个类似的页面,其中 PageModel 位于单独的文件中。 Pages/Index2.cshtml 文件:

@page
@using RazorPages
@model IndexModel2

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

  

Pages/Index2.cshtml.cs ‘代码后置‘ 文件:

using Microsoft.AspNetCore.Mvc.RazorPages;
using System;

namespace RazorPages
{
    public class IndexModel2 : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

  

根据约定, PageModel class 文件与附加了 .cs 的 Razor Page 文件具有相同名称。例如, 代码前置的 Razor Page 是 Pages/Index2.cshtml。 那么包含 PageModel class 的代码后置文件名称就是 Pages/Index2.cshtml.cs

对于简单的页面来说, PageModel class 与 Razor 标记混合使用是很好的选择. 对于更复杂的代码来说, 最好的做法是保持页面与代码分离.

网页路径到页面的关联由页面在文件系统中的位置确定。 下表显示了Razor Page路径和匹配的URL:

文件名称和路径    匹配的 URL
/Pages/Index.cshtml / or /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store or /Store/Index

 

备注:

写一个基础的 form

Razor Pages 功能旨在使与Web浏览器一起使用的常见模式变得容易(Razor Pages features are designed to make common patterns used with web browsers easy)。 Model 绑定, Tag Helpers, and HTML helpers 都可以使用 Razor Page 中的默认属性让它们一起工作 。 研究一个实现了基础的“联系我们” form 的页面的  Contact 模型:

对于本文档中的示例, DbContext 在 Startup.cs 文件中初始化。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;

namespace RazorPagesContacts
{
    public class Startup
    {
        public IHostingEnvironment HostingEnvironment { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AppDbContext>(options =>
                              options.UseInMemoryDatabase("name"));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
}

  

数据模型:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Data
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }
    }
}

  

Pages/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

  

Pages/Create.cshtml.cs 视图文件的后置代码文件:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
    public class CreateModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }
    }
}

  

根据约定, PageModel class 被称为 <PageName>Model 与该页面在同一个命名空间中。在页面中使用 @functions 定义处理程序和使用 PageModel class 不需要太多的改变。

使用 PageModel 后置代码文件支持单元测试, 但要求您编写一个含有显示的构造函数的类。 没有 PageModel 后置代码文件的页面支持运行时编译,这在开发中可能是一个优势.  

该页面包含一个在 POST 请求(用户提交表单时)上运行的 OnPostAsync 方法。 您可以为任何 HTTP 请求添加方法。 最常见的处理方法有:

Async 命名后缀是可选的,但通常用于标识异步方法。 前面示例中的 OnPostAsync 代码通常是写在一个 Controller 中。 上述代码是一个典型的 Razor Pages 代码。 大多数 MVC 原始预防,如 model 绑定, 验证, 和 Action 结果都是共享的。  

前面的 OnPostAsync 方法:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _db.Customers.Add(Customer);
    await _db.SaveChangesAsync();
    return RedirectToPage("/Index");
}

  

OnPostAsync 的基本流程:

检查验证错误。

单输入的数据校验通过后, OnPostAsync 方法会调用 RedirectToPage 帮助方法来返回一个 RedirectToPageResult 实例. RedirectToPage 是一个新的 action 结果, 类似于 RedirectToAction 或 RedirectToRoute,但是它是为 pages 定制的。 在上面的示例中, 它重定向到根目录 Index 页面 (/Index). RedirectToPage 在 页面的URL生成(URL generation for Pages) 部分中有详细说明。

当提交的表单具有验证错误(传递给服务器)时,OnPostAsync 方法会调用 Page 方法. Page 返回一个 PageResult 实例。 返回 Page 类似于在 controllers 中执行返回 View 方法。 PageResult 是处理程序方法的默认 返回类型。一个返回 void 来呈现页面的方法。

自定义 属性使用 [BindProperty] 特性来处理 model 绑定。

public class CreateModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateModel(AppDbContext db)
    {
        _db = db;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        return RedirectToPage("/Index");
    }
}

  

通常情况下,Razor Pages 仅在 non-GET 方法中使用属性绑定。 属性绑定可以减少您编写的代码量。绑定通过使用相同的属性来呈现表单字段 (<input asp-for="Customer.Name" />) 和接受输入,所以可以减少代码量。

主页 (Index.cshtml):

@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customers)
            {
                <tr>
                    <td>@contact.Id</td>
                    <td>@contact.Name</td>
                    <td>
                        <a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
                        <button type="submit" asp-page-handler="delete" 
                                asp-route-id="@contact.Id">delete</button>
                    </td>
                </tr>
            }
        </tbody>
    </table>

    <a asp-page="./Create">Create</a>
</form>

  

后置代码文件 Index.cshtml.cs :

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Pages
{
    public class IndexModel : PageModel
    {
        private readonly AppDbContext _db;

        public IndexModel(AppDbContext db)
        {
            _db = db;
        }

        public IList<Customer> Customers { get; private set; }

        public async Task OnGetAsync()
        {
            Customers = await _db.Customers.AsNoTracking().ToListAsync();
        }

        public async Task<IActionResult> OnPostDeleteAsync(int id)
        {
            var contact = await _db.Customers.FindAsync(id);

            if (contact != null)
            {
                _db.Customers.Remove(contact);
                await _db.SaveChangesAsync();
            }

            return RedirectToPage();
        }
    }
}

  

Index.cshtml 文件包含一下标记,用于为每个节点创建一个编辑链接:

<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>

  

Anchor Tag Helper(锚标记辅助) 使用 asp-route-{value} 属性来生成一个可以跳转到编辑页面的链接。 该链接包含 contact ID 路由数据。 例如, http://localhost:5000/Edit/1.

Pages/Edit.cshtml 文件:

@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@{
    ViewData["Title"] = "Edit Customer";
}

<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name" ></span>
        </div>
    </div>
 
    <div>
        <button type="submit">Save</button>
    </div>
</form>

  

第一行包含 @page "{id:int}" 指令。 路由约束 "{id:int}" 告诉页面接受对包含 int 路由数据的页面的请求。 如果对页面的请求不包含可转换为 int 的路由数据,则运行时返回HTTP 404(未找到)错误。

Pages/Edit.cshtml.cs 文件:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Customer = await _db.Customers.FindAsync(id);

            if (Customer == null)
            {
                return RedirectToPage("/Index");
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new Exception($"Customer {Customer.Id} not found!");
            }

            return RedirectToPage("/Index");
        }
    }
}

  

XSRF/CSRF and Razor Pages

您不必为防伪验证编写任何代码。 Razor Pages自动包含防伪令牌生成和验证。

在 Razor Pages 中使用 Layouts, partials, templates, and Tag Helpers

所有适用于页面上工作的 Razor 视图引擎的功能: Layouts,partials,templates,Tag Helpers,_ViewStart.cshtml_ViewImports.cshtml 的工作方式与传统Razor视图相同。

让我们通过其中的一些特性来梳理一下该页面吧。

添加一个 布局(layout)页面 Pages/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head> 
    <title>Razor Pages Sample</title>      
</head>
<body>    
   <a asp-page="/Index">Home</a>
    @RenderBody()  
    <a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>

  

布局:

更多信息请查看 layout page。

在  Pages/_ViewStart.cshtml 中设置 Layout 属性:

@{
    Layout = "_Layout";
}

  

注意: 布局位于 Pages 文件夹中。分层查找其他视图页面 (layouts, templates, partials) 的起点是当前页面所在的文件夹。在 Pages 文件夹中的布局页可以被位于 Pages 文件夹下的任意 Razor 页面使用。

我们建议您不要把布局文件放在 Views/Shared 文件夹下。 Views/Shared 是一个 MVC 视图模式。 Razor Pages 依赖于文件夹层次结构,而不是路径约定。

来自 Razor 页面的视图查找包括 Pages 文件夹。您使用MVC控制器和常规Razor视图的布局,模板和部分视图可以正常工作(View search from a Razor Page includes the Pages folder. The layouts, templates, and partials you‘re using with MVC controllers and conventional Razor views just work.)。

添加一个 Pages/_ViewImports.cshtml 文件:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

  

@namespace 将在本教程的后面介绍。 @addTagHelper 指令将内置的 built-in Tag Helpers 引入 Pages 文件夹中的所有页面。

当 @namespace 指令在页面上显式使用时:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

  

该指令用于在当前页面中设置命名空间(namespace) 。  @model 伪指令可以不用包含命名空间。

当 @namespace 指令包含在 _ViewImports.cshtml 中时,  @namespace 指令指定的命名空间为需要导入有相同命名空间的页面提供了生成命名空间的前缀。 生成命名空间 (后缀部分) 的其余部分则在包含了 _ViewImports.cshtml 文件的文件夹中的页面中。

例如,  Pages/Customers/Edit.cshtml.cs 代码后置文件中显示的设置了命名空间:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

  

Pages/_ViewImports.cshtml 文件设置以下命名空间:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

  

Pages/Customers/Edit.cshtml Razor 页面中引用的命名空间与代码后置文件中的命名空间相同。 @namespace 指令被设计成能同时在 C# 项目类中和页面代码后置文件中使用,而不需要使用 @using 指令。

注意: @namespace 同样适用于常规 Razor 视图。

原始的 Pages/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

  

更新后的页面:

The Pages/Create.cshtml view file:

@page
@model CreateModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" />
    </form>
</body>
</html>

  

Razor Pages 项目示例 包含配置客户端验证的钩子 Pages/_ValidationScriptsPartial.cshtml

生成 URL 路径

Create 页面中使用 RedirectToPage 来跳转上一页:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _db.Customers.Add(Customer);
    await _db.SaveChangesAsync();
    return RedirectToPage("/Index");
}

  

这款应用有以下“文件/文件夹”结构

成功后,Pages/Customers/Create.cshtml 和 Pages/Customers/Edit.cshtml 页跳转到 Pages/Index.cshtml 页。  /Index 是跳转到前一页的 URI 的一部分。 /Index 可用于生成 Pages/Index.cshtml 页面的 URI。例如:

页面名称是从 /Pages  根文件夹(包括 前导 /,例如 /Index)到页面的路径。上述示例展示了更灵活的URL生成功能,而不仅仅是对URL进行硬编码。使用路由 生成URL,并可以根据自定义的路由在目标路径中生成和编码参数。

支持相对名称生成页面的 URL 。 下表展示了在 Pages/Customers/Create.cshtml 中调用 RedirectToPage 方法生成跳转到不同 Index 页面的方式(使用了不同的参数):

RedirectToPage(x)Page
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index")RedirectToPage("./Index"), 和 RedirectToPage("../Index")  都使用了 相对名称。 RedirectToPage 的参数表示  参考 当前页面的路径计算得到目标页面的名称。  <!-- 评论:原始提供的字符串与当前页面的页面名称组合以计算目标页面的名称。 -- 页面名称,而不是页面路径 -->

构建具有复杂结构的站点时,相对名称链接很有用。 如果使用相对名称在文件夹中的页面之间链接,则可以重命名该文件夹。 所有链接仍然有效(因为它们不包括文件夹名称)。

TempData

ASP.NET Core 公开了 controller 上的 TempData 属性。 此属性存储数据,直到它被读取。Keep 和 Peek 方法可用于在不删除数据的情况下检查数据。当数据被多个请求需要的时候, TempData 对重定向很有用。

[TempData] 在 ASP.NET Core 2.0 中是一个新的属性,且 controllers 和 pages 都支持这个属性。

以下代码就是使用 TempData 来传递 Message 的值。

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

  

以下在 Pages/Customers/Index.cshtml 文件中的标记显示了使用 TempData 存储的 Message 的值。

<h3>Msg: @Model.Message</h3>

  

Pages/Customers/Index.cshtml.cs 后置代码文件将 [TempData] 属性应用到 Message 属性。

[TempData]
public string Message { get; set; }

  

更多信息请查看 TempData 。

Multiple handlers per page

以下页面使用 asp-page-handlerTag Helper 为页面生成两个处理程序标记:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

  

上述示例中的表单有两个提交按钮,每个都使用 FormActionTagHelper 提交到一个不同的 URL。asp-page-handler 属性跟随着 asp-pageasp-page-handler 生成页面需要提交的方法事件的 URL。 没有提到 asp-page 是因为示例是链接到当前页面的。

后置代码文件:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpper();
            return await OnPostJoinListAsync();
        }
    }
}

  

上诉代码使用了 named handler methods. Named handler methods 是通过在 On<HTTP Verb> 和 Async (如果存在) 之间的文本创建的。 在前面的示例中, 页面方法是 OnPostJoinListAsync 和 OnPostJoinListUCAsync。移除 OnPost 和 Async 后,处理程式的名字是 JoinList 和 JoinListUC

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

  

使用上述代码,提交到 OnPostJoinListAsync 的 URL 是 http://localhost:5000/Customers/CreateFATH?handler=JoinList. 提交到 OnPostJoinListUCAsync 的 URL 是 http://localhost:5000/Customers/CreateFATH?handler=JoinListUC.

自定义路由

如果您不喜欢 URL 中的查询字符串 ?handler=JoinList ,则可以更改路由以将处理名称加入到 URL 中。您可以通过在 @page指令之后添加用双引号括起来的路由模板来自定义路由。

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

  

前面的路由将处理名称放入 URL 中,用于替代查询字符串。那 ? 后面的 handler 意味着 route 参数时时可选的。

您可以使用 @page 将额外的部分和参数添加到页面的路由中。无论页面的默认路由附加了什么,都不支持使用绝对或虚拟路径来更改页面的路由(比如 "~/Some/Other/Path") 。

配置和设置

需要配置高级选项,请在 MVC 构建器上使用扩展方法AddRazorPagesOptions :

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            options.RootDirectory = "/MyPages";
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        });
}

  

目前,您可以使用 RazorPagesOptions 设置页面的根目录,或为页面添加应用程序模型约定。 我们希望今后能够实现更多的扩展性。

要预编译视图,请参阅 Razor view compilation 视图编译。

下载或查看示例代码.

请参阅 ASP.NET Core 的 Razor Pages 入门。

原文:http://www.cnblogs.com/ganqiyin/p/7571758.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!