이번 글은 HTML Content를 생성하기 위한 간단한 접근방법을 제공하는 Razor Page에 관한 것입니다. 고전 ASP.NET Web Pages framework의 열망을 어느 정도 유지할 수 있는 Razor Page는 표준에 맞으면서도 세련된 기법을 통해 어떻게 사용할 수 있을지를 알아볼 것입니다. 또한 MVC Framework에서 취한 Controller와 View의 접근법과는 어떻게 다른지, 광범위한 ASP.NET Core platform에서 이들이 어떻게 잘 맞을 수 있는지에 관해서도 함께 다뤄볼 것입니다.
Razor Page가 Controller와 View로 부터의 차이를 어떻게 최소화할 수 있는지에 관해서는 이전 Posting을 통해 언급한 적이 있는데 Razor Page가 그저 MVC의 가벼운 버전이라는 인상을 받고 이들을 무시할 수 있을지도 모르겠습니다. 만약 그렇다면 이건 좋은 생각이 아닌데 Razor Page가 흥미로운 것은 구현된 그들이 구현된 방식이 아닌 개발자의 경험 때문입니다.
특히 MVC 개발자로서 경험이 있다면 Razor Page를 꼭 사용해 보시기 바랍니다. 비록 사용했던 기술은 익숙할 수 있지만 Application기능을 만드는 Process는 다르며 Controller와 View의 규모나 복잡성을 필요로 하지 않는 작고 견고한 기능에 적합하므로 충분히 사용해볼 가치가 있을 것입니다.
1. Project 준비하기
Project는 이전 Posting에서 사용했던 Project를 계속 사용할 것입니다.
2. Razor Page
Razor Page의 작동방식을 배우면 이들이 MVC Framework와 기능이 공유되고 있음을 알 수 있을 것입니다. 사실 Razor Page는 MVC Framework의 간소화로 설명되는데 이것은 사실이지만 이것만으로 Razor Page가 어떤 면에서 유용한지에 대해서는 알기 어려울 수 있습니다.
MVC Framework는 Controller와 응답을 생성하기 위한 View를 선택하면 Action Method를 정의하는 등의 같은 방식을 통해 모든 문제를 해결합니다. 이러한 방식은 매우 유연(Controller는 다른 요청에 응답하는 여러 Action Method를 정의할 수 있고 Action Method는 요청을 처리하기 위해 어떤 View를 사용할지를 결정할 수 있으며 View는 응답을 생성하기 위해 개별적으로나 혹은 공유된 Partial View를 사용할 수 있습니다.) 하기 때문에 효과가 있는 해결책이 될 수 있습니다.
물론 Web Application의 모든 기능이 MVC Framework의 유연성을 필요로 하지는 않으며 대부분의 기능들에서 단일 Action Method는 동일한 View를 통해 넓은 범위의 요청을 처리하는 데 사용될 수 있습니다. 여기서 Razor Page는 그저 C# code와 Markup을 연결하는 보다 집중적인 접근법을 제공하고 있는 것입니다.
그러나 Razor Page도 한계는 있습니다. Razor Page는 단일 기능에 초점을 맞추어 시작하는 경향이 있지만 기능이 향상됨에 따라 서서히 제어할 수 없게 됩니다. 또한 MVC Controller와는 달리 Razor Page는 Web Service(RESTful API 같은)를 생성하는 데 사용할 수 없습니다.
MVC Framework와 Razor Page는 서로 공존할 수 있으므로 굳이 2개의 Model 중에서 하나를 선택할 필요가 없습니다. Razor Page를 통해 자체 기능을 쉽게 개발할 수 있고 Application의 더 복잡한 측면을 MVC Controller와 Action을 사용하여 구현할 수 있습니다.
이제 어떻게 Razor Page를 설정하고 사용하는지를 보고 어떻게 이들이 동작하는지도 알아볼 것입니다. 또한 MVC controller와 action을 통해 이들이 공유하는 공통기반에 대해서도 함께 살펴보겠습니다.
(1) Razor Page 설정
Razor Page를 위한 Application을 준비하기 위해서 Program.cs에 아래 구문을 추가하여 Service를 설정하고 Endpoint routing system을 구성해야 합니다.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
...생략
app.MapDefaultControllerRoute();
app.MapRazorPages();
AddRazorPages method는 Razor Page사용에 필요한 Service를 설정하며 MapRazorPages method는 URL을 page와 일치시키는 routing 구성을 생성합니다.
(2) Razor Page 만들기
Razor Page는 Pages folder에서 정의됩니다. 따라서 Project의 Pages folder를 생성한 뒤 mouse오른쪽 button을 눌러 Add/New Item항목을 선택합니다. 그리고 이어지는 화면에서 Razor Page를 선택한 뒤 Name을 Index.cshtml로 설정하고 Add button을 눌러줍니다.
그러면 해당 file이 생성될 텐데 여기서 cshtml을 아래와 같이 변경하고
@page
@model MyWebApp.Pages.IndexModel
@{
}
@using Microsoft.AspNetCore.Mvc.RazorPages
@using MyWebApp.Models;
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="bg-primary text-white text-center m-2 p-2">@Model.Product?.ProductName</div>
</body>
</html>
같이 생성된 cs file도 역시 아래와 같이 변경합니다.
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
public class IndexModel : PageModel
{
private NorthwindContext context;
public IndexModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task OnGetAsync(long id = 1)
{
Product = await context.Products.FindAsync(id);
}
public Products? Product { get; set; }
}
}
Razor Page는 이전에 설명한 것과 같은 Razor 구문을 그대로 사용할 수 있고 MVC와 동일한 cshtml확장자의 file도 사용할 수 있지만 중요한 몇 가지 차이가 존재합니다.
우선 @page지시자는 Razor Page의 가장 선두에 와야 합니다. 그래야만 file이 Controller와 관련된 VIew로서 오인하지 않기 때문입니다. 이 외에 가장 중요한 차이는 cs file에 있는데 사실 cs file에 구현된 것은 cshtml안에서도 아래와 같이 @function지시자를 통해서 그대로 사용될 수 있습니다.
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="bg-primary text-white text-center m-2 p-2">@Model.Product?.ProductName</div>
</body>
</html>
@functions {
public class IndexModel : PageModel
{
private NorthwindContext context;
public IndexModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task OnGetAsync(int id = 1)
{
Product = await context.Products.FindAsync(id);
}
public Products? Product { get; set; }
}
}
@function은 같은 file안에서 Razor content를 정의하는 C# code를 정의하는 데 사용됩니다. 다만 개인적인 의견으로 Razor Page를 사용하는 경우에도 되도록이면 HTML Content를 표현하는 file(cshtml)과 cs file을 통해 Business loginc을 담는 C# code를 분리하라고 권하고 싶습니다.
Project를 실행하고 /index로 URL을 요청하여 아래와 같은 응답이 생성되는지를 확인합니다.
● URL Routing 규칙
Razor Page에서 URL routing은 Pages folder와 관련된 file명과 위치에 기반합니다. 예제에서 Razor Page의 file명은 Pages folder에서 Index.cshtml인데 이는 /Index요청을 처리하게 된다는 것을 의미합니다. 이러한 Routing규칙은 필요하다면 재정의될 수 있지만 기본적으로 Pages folder가 URL을 통해 응답이 결정되는 Razor Page file의 위치가 됩니다.
● Page Model
Razor Page에서 @model지시자는 Action Method에서 제공된 개체의 유형을 식별하기보다 page model class를 선택하는 데 사용되며 예제에서는 IndexModel class를 선택하고 있습니다.
Page model은 cs file이나 @function 지시자안에서 정의되는데 이때 Page model은 PageModel로부터 파생되는 class입니다.
public class IndexModel : PageModel
Razor Page가 HTTP 요청을 처리할 때면 page model class의 새로운 Instance가 생성되고 생성자 매개변수를 통해 선언된 모든 의존성을 해결하기 위해 의존성주입이 사용됩니다. 예제에서 IndexModel class는 Database의 접근을 처리하는 NorthwindContext에 대한 의존성을 선언하고 있습니다.
Page model 개체가 생성되고 나면 그 이후에는 처리 Method가 호출됩니다. 처리 Method의 이름은 On으로 시작하고 처리할 HTTP 요청의 이름을 따르게 되므로 선택된 Razor page에서 HTTP요청을 처리할때 OnGet method가 호출되는 것입니다. 또한 처리 Method는 비동기가 될 수 있으므로 예제의 경우 GET 요청이 IndexModel class에서 구현된 Method인 OnGetAsync를 호출할 것입니다.
처리 Method의 매개변수 값은 model binding process를 사용한 HTTP 요청으로부터 수집되므로 OnGetAsync method는 model binder로부터 id 매개변수를 위한 값을 받게 됩니다. 이 값은 database로의 질의에 사용되며 그 결과를 Product속성에 할당하고 있습니다.
● Page View
Razor Page는 사용자에게 보여주기 위한 View를 정의하는 content를 만들어내기 위해 표현식과 HTML요소를 혼합하는 MVC와 동일한 방식을 사용합니다. Razor Page에서 model의 method와 속성은 @Model 표현식을 통해 접근할 수 있습니다. 따라서 IndexModel class에 정의된 Produc속성은 다음과 같이 HTML요소의 content를 설정하는 데 사용됩니다.
<div class="bg-primary text-white text-center m-2 p-2">@Model.Product?.ProductName</div>
@Model속성은 IndexModel개체를 반환하며 Product속성에서 반환된 개체의 ProductName속성을 표현식을 통해 읽게 됩니다.
Model속성에서는 항상 page model class의 instance가 할당되고 null이 될 수 없으므로 null조건 연산자를 필요로하지 않습니다. 하지만 page model class에 정의된 속성은 null이 될 수 있으므로 표현식에서 'Product?'처럼 null조건 연산자를 사용해야 합니다.
● C# Class의 생성
이면에서 Razor Page는 일반적인 Razor View처럼 C# class로 변환됩니다. 아래 예제는 Razor Page에서 C# class로 변환된 일부를 나타낸 것입니다.
이전에 MVC에 관한 내용을 알아볼 때 생성되었던 code와 비교해보면 Razor Page가 어떻게 MVC Framework에서 사용되었던 같은 기능에 의존하고 있는지를 알 수 있습니다. HTML요소와 VIew표현식은 WriteLiteral과 Write method를 호출하는 것으로 변환되어 처리됩니다. Razor Page에서 C# class로 처리되는 과정과 일반적인 Razor View에서 처리되는 과정은 동일하다고 볼 수 있습니다.
3. Razor Page Routing
Razor Page는 Routing을 위해 cshtml file의 위치에 의존합니다. 따라서 /index URL로의 요청은 /Pages/Index.cshtml file을 처리하게 되는 것입니다. 만일 Application에서 필요한 URL구조가 있다면 지원하고자 하는 URL segment의 이름으로 folder를 추가해주면 됩니다. 예를 들어 /Pages/Suppliers라는 folder를 만들고 여기에 List.cshtml file을 아래와 같이 추가해 봅니다.
@page
@model MyWebApp.Pages.Suppliers.ListModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using MyWebApp.Models;
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h5 class="bg-primary text-white text-center m-2 p-2">Suppliers</h5>
<ul class="list-group m-2">
@foreach (string s in Model.Suppliers)
{
<li class="list-group-item">@s</li>
}
</ul>
</body>
</html>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
namespace MyWebApp.Pages.Suppliers
{
public class ListModel : PageModel
{
private NorthwindContext context;
public IEnumerable<string> Suppliers { get; set; } = Enumerable.Empty<string>();
public ListModel(NorthwindContext ctx)
{
context = ctx;
}
public void OnGet()
{
Suppliers = context.Suppliers.Select(s => s.CompanyName);
}
}
}
Page model class는 Suppliers속성을 정의하고 있는데 이 속성은 Database에 있는 Supplier개체에 대한 일련의 CompanyName값을 설정합니다. 또한 예제에서 Database와의 상호작용은 동기화이므로 Page model class는 OnGetAsync가 아닌 OnGet Method를 정의하고 있습니다. Project를 실행하고 /suppliers/list로 URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다. 요청한 URL segment의 경로는 folder와 List.cshtml Razor Page file명에 해당합니다.
기본 URL 처리
MapRazorPages method는 Index.cshtml Razor Page를 위한 기본 URL Route를 설정하는데 MVC Framework와 비슷한 규칙을 따릅니다. 이것은 Project에 추가된 첫 번째 Razor Page가 보통 Index.cshtml file이 되기 때문입니다. 그러나 Project가 Razor Page와 MVC Framework를 같이 사용하는 경우라면 Razor Page에 의해 정의된 기본 Route가 더 낮은 순서로 생성되었으므로 더 우선하게 됩니다. 따라서 기본 경로인 / 나 /index 로의 URL요청은 Razor Page의 Index.cshtml file을 처리하게 되는데 현재 작성 중인 예제가 그렇게 작동합니다.
따라서 만약 MVC Framework가 기본 URL로 처리되기를 원한다면 Razor Page Route의 순서를 다음과 같이 변경해야 합니다.
app.MapRazorPages().Add(b => ((RouteEndpointBuilder)b).Order = 2);
참고로 Razor Page Route는 순서 0으로 생성됩니다. 그리고 MVC Route가 1로 생성되므로 더 앞선 번호가 부여되는 것이고 위 예제를 통해 MVC Framework가 더 앞선 번호를 가질 수 있도록 변경되는 것입니다.
예제에서는 현재 Razor Page와 MVC Framework를 같이 사용하고 있으므로 MVC Framework가 기본 URL로 처리되도록 의도적으로 Order를 바꾸었습니다.
(1) Razor Page에서 Routing Pattern 지정
Routing 수행을 위해 folder와 file구조를 사용한다는 것은 Model Binding 처리를 사용하기 위한 segment변수가 없다는 것을 의미합니다. 대신 요청을 처리할 Method의 값은 URL Query 문자열로부터 가져올 수 있습니다. 따라서 Project를 실행하고 /index?id=2 URL을 요청하게 되면 다음과 같은 결과를 볼 수 있습니다.
예제에서 query 문자열은 id라는 이름의 매개변수를 전달하고 있으며 해당 값은 Index Razor Page의 OnGetAsync Method에 정의된 id 매개변수로 model binding process에 의해 전달됩니다. model binding에 관해서는 추후에 설명하겠지만 지금은 요청 URL에 있는 query string매개변수가 OnGetAsync가 호출될 때 id인수로 전달되고 Product에 대한 Database질의에 사용된다는 것만 기억하면 충분합니다.
여기서 @Page지시자는 routing pattern에 사용될 수 있으며 아래와 같이 segment변수를 정의할 수 있습니다.
@page "{id:int?}"
@model MyWebApp.Pages.IndexModel
모든 URL Pattern기능은 @page지시자와 함께 사용될 수 있습니다. 예제에서 사용된 Route Pattern은 id라는 이름의 선택적 매개변수를 추가한 것으로 단지 int으로 읽어 들일 수 있는 segment만이 일치되도록 제한됩니다. Project를 실행하여 /index/4 URL을 요청하여 다음과 같은 결과가 나오는지 확인합니다.
@Page지시자는 또한 아래와 같이 Razor Page를 위한 file기반 Routing규칙을 재설정할 수도 있습니다.
@page "/lists/suppliers"
@model MyWebApp.Pages.Suppliers.ListModel
위 예제는 Pages/Suppliers folder에 있는 List.cshtml에 @Page지시자를 적용한 것으로 List Page의 Route를 변경하게 되므로 /lists/suppliers URL과 일치하게 됩니다.
(2) Razor Page에 Route 추가하기
@page지시자의 사용은 Razor Page의 file기반 route의 기본적인 동작을 바꾸게 됩니다. 따라서 만약 Page에서 여러 route를 정의하고자 한다면 Program.cs에 아래와 같이 설정 구문을 추가해야 합니다.
using MyWebApp.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
...생략...
builder.Services.Configure<RazorPagesOptions>(opts => {
opts.Conventions.AddPageRoute("/Index", "/extra/page/{id:int?}");
});
var app = builder.Build();
option pattern은 RazorPageOptions class를 사용해 Razor Page에 별도의 route를 추가하는 데 사용되며 여기서 Conventions속성에서 호출되는 AddPageRoute 확장 method가 Page에 route를 추가하게 됩니다. 이때 첫 번째 인수는 page의 경로이며 file확장자가 없고 Pages folder의 상대적인 경로가 됩니다. 두 번째 인수는 Routing 설정에 추가하기 위한 URL Pattern입니다.
위와 같이 설정한 뒤 Project를 실행하여 /extra/page/2 URL을 요청합니다. 이 URL은 위에서 추가한 URL Pattern과 일치되며 이전과 동일한 응답을 생성하게 됩니다. 또한 이전 예제에서 추가된 route는 @Page속성을 통해 정의된 route를 보완하게 되므로 /index/2 URL요청에도 역시 동일한 응답을 생성하게 됩니다.
4. Page Model Class
PageModel class로부터 파생된 Page Model은 ASP.NET Core의 나머지와 Razor Page의 View부분 간에 연결고리를 제공합니다. 또한 요청을 처리하는 방식을 관리하기 위한 Method와 context data를 제공하기 위한 여러 속성을 포함하고 있는데 아래 표에서 상세한 목록을 확인해 볼 수 있습니다. 물론 사용 가능한 모든 것을 나열한 것은 아니지만 Razor Page개발 시 종종 필요한 것들이며 Page의 View부분을 Render 하는데 필요한 Data를 선택하는데 더 중점을 둡니다.
HttpContext | 이 속성은 HttpContext 개체를 반환합니다. |
ModelState | 이 속성은 Model Binding과 Validation기능으로의 접근을 제공합니다. |
PageContext | 이 속성은 현재 Page선택에 대한 추가적인 정보와 함께 PageModel class에서 정의된 동일한 다수의 속성으로의 접근을 제공하 PageContext 개체를 반환합니다. |
Request | 이 속성은 현재 HTTP요청을 나타내는 HttpRequest개체를 반환합니다. |
Response | 이 속성은 현재 응답을 나나태는 HttpResponse개체를 반환합니다. |
RouteData | 이 속성은 routing system과 일치되는 Data의 접근을 제공합니다. |
TempData | 이 속성은 다음 요청에서 읽어들일 수 있을때까지 data를 저장하기 위한 temp data로의 접근을 제공합니다. |
User | 이 속성은 요청과 관련된 사용자를 나타내는 개체를 반환합니다. |
(1) Code-Behind Class File
@function지시자는 같은 file안에서 business loginc과 Razor content가 같이 정의될 수 있도록 하는 것이며 React나 Vue처럼 client-side framework에서 사용되는 개발 방법이기도 합니다.
같은 file에서 code와 markup을 사용하는 것은 편리할 수 있지만 복잡한 Application일수록 관리하기가 어려워질 수 있습니다. 하지만 Razor Page는 code과 View를 분리할 수 있도록 지원하는데 MVC와 비슷하며 code-behind로 알려진 file에서 C# class가 정의되는 ASP.NET Web Page를 연상시키기도 합니다. 지금까지의 예제에서도 code-behind를 사용했는데 cshtml에서
@model MyWebApp.Pages.IndexModel
처럼 code에서 정의된 C# class롤 @model표현식으로 page model의 namespace와 함께 지정하는 방법으로 분리가 가능해집니다.
Razor Pages code-behind file의 naming규칙은 .cs file확장자가 View file의 이름으로 추가되는 것이며 Index.csthml file이 Project에 추가되면서 Razor Page Template에 의해 생성됩니다.
● View Imports File 추가
View Imports File을 사용하면 View에 있는 Page Model class에서 완전한 Namespace를 일일이 작성할 필요가 없습니다. 사실상 MVC Framework와 동일한 규칙을 사용하는 것입니다. View Imports File은 Razor View Imports template을 사용해 _ViewImports.cshtml이름으로 Project의 Pages folder에 아래와 같이 추가합니다.
@namespace MyWebApp.Pages
@using MyWebApp.Models
예제에서 @namespace지시자는 View에서 생성된 C# class에서 사용할 Namespace를 설정하는데 view imports file에서 지시자의 사용은 View와 Page model class가 같은 Namespace에 있는 것처럼 Application에 있는 모든 Razor Page를 위한 기본 Namespace를 설정하게 되므로 아래 Index.cshtml처럼 @model지시자에서는 완전한 Namespace를 명시할 필요가 없게 되었습니다.
@page "{id:int?}"
@model IndexModel
(2) Razor Page에서의 Anction Result
Razor Page handler method는 생성하는 응답을 제어하기 위해 같은 IActionResult interface를 사용합니다. Page model class를 쉽게 개발하기 위해 handler method는 Page의 View영역에 무엇인가를 표시하기 위한 암시적인 결과를 가집니다. 아래 예제는 명시적인 결과를 사용하도록 Pages에 잇는 Index을 변경한 것입니다.
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Pages
{
public class IndexModel : PageModel
{
private NorthwindContext context;
public IndexModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task<IActionResult> OnGetAsync(int id = 1)
{
Product = await context.Products.FindAsync(id);
return Page();
}
public Products? Product { get; set; }
}
}
예제에서 Page Method는 PageModel class로부터 파생된 것이며 framework가 Page의 View부분을 render 하도록 하는 PageResult개체를 생성합니다. MVC Action Method에서 사용한 View와는 달리 Razor Page Method는 인수를 수용하지 않으며 항상 요청을 처리하기 위해 선택한 Page의 View를 render 합니다.
PageModel class는 다른 Action결과를 만들 수 있는 아래 표의 또 다른 Method를 제공하여 필요한 여러 가지 결과를 만들어 낼 수 있습니다.
Page() | 해당 Method에 의해 반환된 IActionResult는 200 OK 상태 code를 생성하며 Razor Page의 View를 render합니다. |
NotFound() | 해당 Method에 의해 반환된 IActionResult는 404 Not Found상태 code를 생성합니다. |
BadRequest(state) | 해당 Method에 의해 반환된 IActionResult는 400 Bad Rquest 상태 code를 생성합니다. 또한 client에게 문제를 설명할 수 있는 선택적 model 상태 개체를 수용할 수 있습니다. |
File(name, type) | 해당 Method에 의해 반환된 IActionResult는 200 OK 상태 code를 생성하며 Content-Type header를 지정된 type으로 설정하고 client에 설정된 file을 전송합니다. |
Redirect(path) RedirectPermanent(path) |
해당 Method에 의해 반환된 IActionResult는 302 Found와 301 Moved Permanently응답을 생성하여 client를 지정한 URL로 redirect합니다. |
RedirectToAction(name) RedirectTo ActionPermanent(name) |
해당 Method에 의해 반환된 IActionResult는 302 Found와 301 Moved Permanently응답을 생성하여 client를 지정한 Action Method로 redirect합니다. client의 redirect를 위해 사용하는 URL은 Routing기능을 통해 생성됩니다. |
RedirectToPage(name) RedirectToPagePermanent(name) |
해당 Method에 의해 반환된 IActionResult는 302 Found와 301 Moved Permanently응답을 생성하여 client를 다른 Razor Page로 redirect합니다.만약 지정한 이름의 Razor Page가 존재하지 않는다면(찾을 수 없다면) client는 현재 Page로 redirect하게 됩니다. |
StatusCode(code) | 해당 Method에 의해 반환된 IActionResult는 지정한 상태 code를 통한 응답을 생성합니다. |
● Action Result 사용
Page Method를 제외하고 위 표의 Method는 Action Method에서 사용할 수 있는 Method들과 동일합니다. 그러나 상태 Code응답을 보내는 것은 Razor Page에서는 도움되지 않습니다. 이는 client가 View의 Content를 예상할 수 있는 경우에만 사용될 수 있기 때문입니다.
따라서 찾을 수 없는 Data에 대한 요청이 들어왔을 때는 NotFound method를 사용하는 것보다는 client에게 HTML형식으로 된 오류 message를 표시하기 위해 다른 URL로 redirect 시키는 편이 좋습니다. Redirection은 정적 file이 될 수 있고 다른 Razor Page혹은 Controller에 정의된 다른 action이 될 수도 있습니다. 이와 관련된 내용을 알아보기 위해 Project의 Pages folder에 NotFound.cshtml file을 아래와 같이 추가합니다.
@page "/noid"
@model NotFoundModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using MyWebApp.Models;
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<title>Not Found</title>
</head>
<body>
<div class="bg-primary text-white text-center m-2 p-2">No Matching ID</div>
<ul class="list-group m-2">
@foreach (Products p in Model.Products)
{
<li class="list-group-item">@p.ProductName (ID: @p.ProductId)</li>
}
</ul>
</body>
</html>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
public class NotFoundModel : PageModel
{
private NorthwindContext context;
public IEnumerable<Products> Products { get; set; } = Enumerable.Empty<Products>();
public NotFoundModel(NorthwindContext ctx)
{
context = ctx;
}
public void OnGetAsync()
{
Products = context.Products;
}
}
}
예제에서 @Page지시자는 Routing 규칙을 재정의하였으므로 해당 Razor Page는 /noid URL요청을 처리할 것입니다. 또한 Page Model class는 Entity Framework Core context object를 통해 Database에 질의를 수행하고 Database에 있는 Product의 이름과 ID값에 대한 List를 화면에 표시하게 됩니다.
아래 예제에서는 IndexModel class의 Method를 변경하여 Database에 있는 Product개체와 일치하지 않는 요청이 도달하면 사용자를 NotFound Page로 Redirect를 수행하도록 하였습니다.
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Pages
{
public class IndexModel : PageModel
{
private NorthwindContext context;
public IndexModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task<IActionResult> OnGetAsync(int id = 1)
{
Product = await context.Products.FindAsync(id);
if (Product == null)
return RedirectToPage("NotFound");
return Page();
}
public Products? Product { get; set; }
}
}
RedirectToPage method는 다시 client를 다른 Razor Page로 Redirect 시키는 Action Result를 생성하게 됩니다. 이때 RedirectToPage method에서 지정된 Page는 확장자가 없으며 모든 folder의 구조는 Pages folder에서 상대적으로 지정됩니다. Project를 실행하여 /index/500로 URL을 요청합니다. 이는 id segment변수에 500 값을 지정하게 되든데 Database의 어떠한 Product로 일치하지 않는 값입니다. 따라서 Browser는 Redirect를 수행하여 다음과 같은 결과를 표시하게 됩니다.
예제에서 Routing System은 @Page지시자를 통해 routing pattern을 지정하여 client가 Redirect 될 URL을 생성하도록 하였습니다. 사용한 RedirectToPage method의 인수는 NotFound이지만 이 것은 @Page지시자를 통해 지정된 /noid 경로로 전환되도록 합니다.
(3) 다중 HTTS Method 처리
Razor Pages에서는 다른 HTTP Method로 응답할 수 있는 처리 Method를 정의할 수 있습니다. 가장 일반적으로는 GET과 POST method를 결합하여 사용자가 Data를 확인하고 수정할 수 있도록 하는 것입니다. Project에서 Pages folder에 Editor.cshtml file을 아래와 같이 추가합니다.
예제에서는 가능한 한 예제를 간소하게 하려고 하지만 HTML Form을 생성하고 Submit수행 시 Data를 전송받기 위한 훌륭한 ASP.NET Core기능이 존재합니다.
@page "{id:int}"
@model EditorModel
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="bg-primary text-white text-center m-2 p-2">Editor</div>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr><th>Name</th><td>@Model.Products?.ProductName</td></tr>
<tr><th>Price</th><td>@Model.Products?.UnitPrice</td></tr>
</tbody>
</table>
<form method="post">
@Html.AntiForgeryToken()
<div class="form-group">
<label>Price</label>
<input name="price" class="form-control" value="@Model.Products?.UnitPrice" />
</div>
<button class="btn btn-primary mt-2" type="submit">Submit</button>
</form>
</div>
</body>
</html>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
public class EditorModel : PageModel
{
private NorthwindContext context;
public Products? Products { get; set; }
public EditorModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task OnGetAsync(int id)
{
Products = await context.Products.FindAsync(id);
}
public async Task<IActionResult> OnPostAsync(int id, decimal price)
{
Products? p = await context.Products.FindAsync(id);
if (p != null)
p.UnitPrice = price;
await context.SaveChangesAsync();
return RedirectToPage();
}
}
}
예제에서 Razor Page요소는 간단한 HTML Form을 생성하고 있는데 이를 통해 사용자에게 Products개체의 Price속성에 대한 값을 입력할 수 있는 Input요소를 제공하고 있습니다. form요소는 action속성 없이 정의되었는데 이는 사용자가 Submit button을 click 하면 Browser가 POST 요청을 Razor Page의 URL로 전송한다는 것을 의미합니다.
@Html.AntiForgeryToken() 표현식은 hidden form field를 HTML Form안에 추가하도록 합니다. 이를 통해 ASP.NET Core는 cross-site request forgery (CSRF) 공격으로부터 Application을 보호할 수 있도록 합니다. 이와 관련해서는 추후에 자세히 알아볼 것입니다.
Page Model class는 2개의 처리 method를 정의하고 있는데 Method의 이름 자체가 Razor Page Framework에게 각 HTTP Method가 됨을 알려주고 있습니다. 따라서 OnGetAsync Method는 GET 요청을 처리하는 데 사용되어 사용자에게 Products개체에 대한 상세 내용을 표시하고 OnPostAsync Method는 사용자가 HTML Form을 Submit 할 때 Browser를 통해 전송되는 POST 요청을 처리합니다. 이때 OnPostAsync의 매개변수는 요청으로부터 가져오게 되므로 id값은 URL에서, price값은 Form으로부터 가져오게 됩니다.
POST Redirection
예제에서 OnPostAsync Method의 마지막에서는 RedirectToPage Method가 사용되었음에 주목하시기 바랍니다. Method를 호출할 때는 어떠한 인수도 사용하지 않았으므로 사용자를 Razor Page의 URL로 Redirect 하게 됩니다. 다소 이상해 보일 수 있지만 이는 Browser가 POST 요청에 사용한 URL로 GET 요청을 보내도록 합니다. 이러한 유형의 Redirection은 사용자가 Browser를 새로고침 할 때 우연하게라도 하나이상의 같은 action이 수행되는 것을 막게 됨으로써 Browser가 POST 요청을 Submit하지 않게 합니다.
Project를 실행하여 /editor/1으로 URL을 요청합니다. 그리고 Input Field의 값을 100으로 변경한 뒤 Submit button을 눌러줍니다. 그러면 Browser는 POST 요청을 OnPostAsync Method로 잔달하여 Database를 변경하고 다시 Browser는 Redirect를 수행함으로써 변경된 값을 표시하게 됩니다.
(4) 처리 Method 선택
Page Model class는 여러 처리 Method를 정의할 수 있고 이에 따라 handler query string 매개변수 혹은 routing segment 변수를 통해서 요청이 처리 Method를 선택할 수 있습니다. Project Pages folder에 HandlerSelector.cshtml이름의 Razor Page를 아래와 같이 추가합니다.
@page
@model HandlerSelectorModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.EntityFrameworkCore
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="bg-primary text-white text-center m-2 p-2">Selector</div>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr><th>Name</th><td>@Model.Product?.ProductName</td></tr>
<tr><th>Price</th><td>@Model.Product?.UnitPrice</td></tr>
<tr><th>Category</th><td>@Model.Product?.Category?.CategoryName</td></tr>
<tr><th>Supplier</th><td>@Model.Product?.Supplier?.ContactName</td></tr>
</tbody>
</table>
<a href="/handlerselector" class="btn btn-primary">Standard</a>
<a href="/handlerselector?handler=related" class="btn btn-primary">Related</a>
</div>
</body>
</html>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
public class HandlerSelectorModel : PageModel
{
private NorthwindContext context;
public Products? Product { get; set; }
public HandlerSelectorModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task OnGetAsync(int id = 1)
{
Product = await context.Products.FindAsync(id);
}
public async Task OnGetRelatedAsync(int id = 1)
{
Product = await context.Products.Include(p => p.Supplier).Include(p => p.Category).FirstOrDefaultAsync(p => p.ProductId == id);
if (Product != null && Product.Supplier != null)
{
Product.Supplier.Products = null;
}
if (Product != null && Product.Category != null)
{
Product.Category.Products = null;
}
}
}
}
예제에서 Page Model class는 OnGetAsync와 OnGetRelatedAsync라는 2개의 처리 Method를 정의하고 있습니다. OnGetAsync Method를 기본적으로 사용되는 Method로서 Project를 실행하고 /handlerselector URL을 요청하여 사용할 수 있습니다. 해당 Method는 Database로 질의하여 그 결과를 사용자에게 아래와 같이 표시할 것입니다.
해당 Page에서 Render 된 anchor요소 중 하나는 아래와 같이 handler query string 매개변수를 통해 URL을 지정하고 있습니다.
<a href="/handlerselector?handler=related" class="btn btn-primary">Related</a>
여기서 처리 Method의 이름은 On접두사와 Async접미사 없이 지정되므로 OnGetRelatedAsync Method가 related의 처리 값을 사용해 선택하고 있습니다. 이 대체 처리 Method는 query에 관련된 Data를 포함하고 있으며 사용자에게 추가적인 Data를 다음과 같이 표시하게 됩니다.
5. Razor Page View
Razor Page의 View는 Controller를 통해 사용했던 View에서와 같은 구문, 같은 기능을 사용합니다. 폭넓은 표현식과 Session, Temp data 그리고 Layout 같은 기능 역시 사용할 수 있으며 @Page지시자와 Page model class의 사용과는 별개로 Layout이나 Partial View와 같은 기능 설정은 거이 동일하다고 할 수 있습니다.
(1) Razor Page를 위한 Layout 설정
Razor Page의 Layout은 Pages/Shared에 생성된다는 것만 제외하면 Controller View에서의 방법과 동일하다고 할 수 있습니다. Project에서 Pages/Shared folder를 만들고 그 안에 Razor Layout template을 통해서 _Layout.cshtml이름의 file을 아래와 같이 생성합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<h5 class="bg-secondary text-white text-center m-2 p-2">
Razor Page
</h5>
@RenderBody()
</body>
</html>
Layout은 Razor Page와 관련된 어떠한 기능도 사용하지 않으며 Controller View를 생성할 때 사용된 것과 동일한 표현식 및 요소를 포함합니다. 다음으로 Pages folder에 Razor View Start template으로 _ViewStart.cshtml file을 아래와 같이 생성합니다.
@{
Layout = "_Layout";
}
Razor Page에서 생성된 C# class는 Page class로부터 파생되며 여기서 View start file에서 사용된 Layout속성을 제공하는데 이것은 Controller view에서 사용된 Layout속성과 같은 목적으로 사용됩니다. 이제 Index.cshtml을 변경하여 Layout에서 제공되는 요소를 아래와 같이 제거해 줍니다.
@page "{id:int?}"
@model IndexModel
@{
}
<div class="bg-primary text-white text-center m-2 p-2">@Model.Product?.ProductName</div>
View start file을 사용함으로써 모든 Page에 Layout을 적용하게 되는데 이때 Page는 Layout에 할당된 값을 재정의하지 않습니다. 따라서 만약 Layout을 사용하지 않는다면 아래와 같이 Layout속성에 null을 할당해야 합니다.
@page "{id:int}"
@model EditorModel
@{
Layout = null;
}
Project를 실행하여 /index URL을 요청하면 Layout이 사용된 결과를 볼 수 있습니다. 또한 /editor/1 URL을 요청하게 되면 Layout이 사용되지 않은 결과 또한 확인할 수 있습니다.
(2) Partial Views
Razor Page는 또한 Partial View를 사용할 수 있으므로 공통된 content를 중복시킬 필요가 없습니다. 이번 예제는 Tag helper기능에 의존하므로 아래와 같이 View import file에 이와 관련된 지시자를 추가하여 사용자 정의 HTML요소를 Partial View에 적용할 수 있도록 합니다.
@namespace MyWebApp.Pages
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
다음으로 Pages/Shared folder에 _ProductPartial.cshtml이름의 Razor View를 아래와 같이 추가합니다.
@model Products
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr><th>Name</th><td>@Model?.ProductName</td></tr>
<tr><th>Price</th><td>@Model?.UnitPrice</td></tr>
</tbody>
</table>
</div>
Partial View 역시 Razor Page와 관련된 것은 아무것도 없습니다. Partial view는 @model 자시자를 사용하여 view model 개체를 전달받는데 이때 Razor Page와 관련된 @Page 지시자는 사용하지 않고 또한 page model 역시 가지지 않습니다. 이러한 방식은 Razor Page가 MVC Controller와 함께 Partial View를 공유할 수 있도록 합니다.
Partial View 검색 위치
Razor View Engine은 partial view를 Razor Page를 사용하는 곳과 같은 위치에서 검색하기 시작합니다. 만약 여기서 일치되는 file이 존재하지 않는다면 Pages folder에 도달할 때까지 각각의 부모 directory를 검색하게 됩니다. 따라서 만약 Pages/App/Data folder에서 정의된 Razor Page에서 Partial View가 사용되었다면 View engine은 Pages/App/Data folder를 먼저 찾은 다음 /Pages/App folder 그리고 /Pages folder를 순서대로 찾게 됩니다. 만약 이렇게 해서도 file이 발견되지 않는다면 Pages/Shared folder를 검색하고 그다음 최종적으로 View/Shared folder에서 검색을 수행합니다.
이와 같은 검색방식으로 인해 Controller에서 사용되기 위한 partial view가 Razor Page에서도 사용될 수 있도록 하는데 결과적으로 MVC Controller와 Razor Page둘다 사용되는 Project에서 Application의 content중복을 최소화하도록 하는 유용한 기능으로 사용할 수 있습니다.
Partial View는 아래와 같이 partial요소를 통해 적용되며 이때 name은 view의 이름을 특정하는 속성이며 model은 view model을 제공하는 속성입니다.
Partial View는 view model을 page model이 아닌 자체 @model 지시자를 통해 전달받습니다. 이는 model 속성의 값이 Model이 아닌 Model.Product이기 때문입니다.
@page "{id:int?}"
@model IndexModel
@{
}
<div class="bg-primary text-white text-center m-2 p-2">@Model.Product?.ProductName</div>
<partial name="_ProductPartial" model="Model.Product" />
Razor Page가 응답을 처리할 때 partial view의 content가 응답에 포함됩니다. Project를 실행하면 다음과 같이 Partial View에서 정의된 Table이 응답에 포함되었음을 확인할 수 있습니다.
(3) Page Model 없이 Razor Page 생성하기
Razor Page가 사용자에게 비교적 간소한 data를 제공하는 경우라면 View에서 사용되는 속성을 정의하기 위한 생성자 의존성을 간소하게 선언한 Page Model이 될 수 있습니다. 이러한 Pattern을 이해하기 위해 Project의 Pages에 Data.cshtml이름의 Razor Page를 아래와 같이 추가합니다.
@page
@model MyWebApp.Pages.DataModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@{
}
<h5 class="bg-primary text-white text-center m-2 p-2">Categories</h5>
<ul class="list-group m-2">
@foreach (Categories c in Model.Categories)
{
<li class="list-group-item">@c.CategoryName</li>
}
</ul>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
public class DataModel : PageModel
{
private NorthwindContext context;
public IEnumerable<Categories> Categories { get; set; } = Enumerable.Empty<Categories>();
public DataModel(NorthwindContext ctx)
{
context = ctx;
}
public void OnGet()
{
Categories= context.Categories;
}
}
}
예제에서 page model은 의존 주입을 통해 data에 View의 접근을 제공하는 것 이외에 data를 변환하거나 계산과 같은 어떠한 작업도 수행하지 않습니다. Page model이 단지 service로의 접근만을 제공하는데만 사용되는 이와 같은 pattern은 @inject 지시자를 사용하여 service로의 접근성을 획득하고 page model의 필요성을 제거함으로서 회피할 수 있습니다.
@inject지시자의 사용은 가급적 피해야 하며 page model class가 service로의 접근을 제공하는것 이외에 값을 추가하지 않는 경우에만 사용되어야 합니다. 이외 다른 모든 상황에서 page model이 더 유지 관리하기가 쉬울 것입니다.
아래 예제는 Data.cshtml에서 cs file을 제거하고 cshtml만 아래와 같이 변경한 것입니다.
@page
@inject Models.NorthwindContext context;
@{
}
<h5 class="bg-primary text-white text-center m-2 p-2">Categories</h5>
<ul class="list-group m-2">
@foreach (Categories c in context.Categories)
{
<li class="list-group-item">@c.CategoryName</li>
}
</ul>
@inject 표현식은 service가 접근되는 service유형과 이름을 특정합니다. 예제에서 service의 유형은 NowthwindContext이며 접근되는 이름은 context입니다. view안에서 @foreach표현식은 NothwindContext.Categoies 속성에서 반환된 각 개체의 요소를 생성하고 있습니다. 예제에서 page model이 사용되지 않았으므로 @page와 @using지시자는 제거되었습니다. project를 실행하고 /data로 URL을 요청하여 아래와 같은 응답이 생성되는지를 확인합니다.
'.NET > ASP.NET' 카테고리의 다른 글
ASP.NET Core - 14. Tag Helper (0) | 2023.01.08 |
---|---|
ASP.NET Core - 13. View Component (0) | 2023.01.01 |
ASP.NET Core - 11. View와 Controller - 2 (0) | 2022.12.17 |
ASP.NET Core - 10. View와 Controller - 1 (0) | 2022.12.11 |
ASP.NET Core - 9. 고급 Web Service 기능 (0) | 2022.12.04 |