1. Category 탐색 기능
이전 예제 Project의 제품 표시에는 단순히 제품만 나열할 뿐이지만 제품의 category별로 해당 제품을 살펴볼 수 있도록 하면 사용자에게 좀 더 편리함을 제공해 줄 수 있을 것입니다.
(1) Product List filtering
이를 구현하기 위해 Models->ViewModels folder에서 ProductsListViewModel.cs를 수정하여 아래와 같이 현재 선택된 CurrentCategory속성을 추가합니다.
public class ProductsListViewModel
{
public IEnumerable<Product> Products { get; set; } = Enumerable.Empty<Product>();
public PageInfo PageInfo { get; set; } = new();
public string? CurrentCategory { get; set; }
}
그리고 HomeController의 Index Action Method에 category 매개변수를 추가하여 Product List가 Category별로 Filtering 될 수 있도록 합니다. 이때 위에서 추가한 CurrentCategory속성을 통해 현재 선택된 Category도 같이 확인할 수 있도록 합니다.
public IActionResult Index(string? category, int currentPage = 1)
{
var result = _repository.Products
.Where(p => p.Category == category || category == null)
.OrderBy(p => p.Id)
.Skip((currentPage - 1) * PageSize)
.Take(PageSize);
return View(
new ProductsListViewModel
{
Products = result,
PageInfo = new PageInfo
{
CurrentPage = currentPage,
ItemsPerPage = PageSize,
TotalItems = _repository.Products.Count()
},
CurrentCategory = category,
});
}
project를 실행하고 URL을 통해 category를 지정하면 해당 Product List가 표시될 것입니다.
(2) URL Scheme 변경
URL은 '?category=Peripheral'와 같이 다소 번잡한 느낌을 가질 수 있는데 이에 대한 개선을 위해 Program.cs를 수정하여 Routing Schema를 아래와 같이 구성해 줍니다.
app.UseStaticFiles();
app.MapControllerRoute("categoryPage", "{category}/Page{currentPage:int}", new { Controller = "Home", action = "Index" });
app.MapControllerRoute("page", "Page{currentPage:int}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("category", "{category}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("default", "Products/Page{currentPage}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapDefaultControllerRoute();
첫 번째 Route는 category와 page를 모두 지정하는 경우 두 번째는 page만 지정하는 경우 세 번째는 category만 지정하는 경우 등에 대한 URL Route를 설정한 것이며 그 외에 다른 경우(예:/만 있고 category나 page값이 지정되지 않은 경우)는 default Route가 적용되도록 하였습니다.
ASP.NET Core Routing system은 사용자로부터 들어오는 요청을 처리하기도 하지만 또한 URL scheme와 일치하고 Web Page내부에서 표현 가능한 URL을 생성하기도 합니다. 이렇듯 Routing system을 들어오는 요청과 그에 맞는 URL을 생성하는 데 사용하면 모든 URL을 일관적으로 표현될 수 있습니다.
IUrlHelper Interface는 URL생성기능으로의 접근을 제공하고 있는데 이전에 생성한 Tag Helper에서 해당 Interface와 Action Method를 사용하였습니다. 이제 여기에 category와 관련한 URL 생성을 다시 적용해야 하는데 Tag Helper class에서 View로부터 추가적인 정보를 받을 수 있도록 아래와 같이 공용 접두사를 가진 속성을 추가해 줍니다.
[HtmlAttributeName(DictionaryAttributePrefix = "page-url-")]
public Dictionary<string, object> PageUrlValues { get; set; } = new Dictionary<string, object>();
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (ViewContext != null && PageModel != null)
{
IUrlHelper urlHelper = _urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i = 1; i <= PageModel.TotalPages; i++)
{
TagBuilder tag = new TagBuilder("a");
PageUrlValues["currentPage"] = i;
tag.Attributes["href"] = urlHelper.Action(PageAction, PageUrlValues);
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
추가된 속성은 단일 Collection에서 모든 요소를 전달받을 수 있도록 하는 속성으로 HtmlAttributeName으로 Decorating된 attribute는 element상에서 접두사를 통해 attribute를 특정할 수 있도록 합니다. 예제에서는 이 값을 page-url-로 지정하였는데 해당 값으로 시작하는 모든 attribute의 값이 dictionary에 추가되고 PageUrlValues속성에 할당될 것입니다. 그리고 이 값은 IUrlHelper.Action() method로 전달되어 tag helper에서 만들게 되는 a element에서 href attribute를 위한 URL을 생성할 수 있도록 합니다.
이제 Tag Helper에 의해 처리되는 div element를 수정해 URL을 생성하는데 사용될 category를 다음과 같이 지정해줍니다.
<div page-model="@Model?.PageInfo" page-action="Index" page-url-category="@Model?.CurrentCategory!"></div>
참고로 CurrentCategory에서 !인 null-forgiving연산자는 null값을 그대로 전달하도록 명시한 것이며 이는 null에 대한 별도의 설정 없이도 compiler의 경고를 제거할 수 있도록 합니다. 또한 page-url-category attribute에 Model을 통하여 현재 지정된 Category명을 설정하였는데 이러한 구현을 통해 Tag Helper의 PageUrlValues에서는 category로 설정한 모든 값이 할당될 것입니다.
이전에는 Link의 URL이 '/page1'으로만 생성되었으나 수정한 결과에서는 '/Computer/page1'처럼 URL을 생성할 수 있게 하였으므로 project를 시작하고 URL을 'https://localhost:7275/computer/'와 같이 지정하여 LInk에 정상적인 Category명을 포함하는 URL을 생성하도록 합니다.
(3) Category Navigation Menu 만들기
URL을 통해서는 Category를 나눌 수 있으나 사용자에게 URL을 직접 typing하도록 요구할 수는 없으므로 사용자가 선택 가능하고 현재 선택된 Cateogry를 표시하는 Category Menu를 직접 추가해야 합니다.
ASP.NET Core는 이를 위해 view components라는 기능을 가지고 있는데 이것은 지금 추가할 navigation menu과 같은 Item을 생성하기에 적합한 선택이 될 수 있습니다. View Component는 C# class로서 Razor partial view를 선택하고 표시할 수 있는 기능을 구현할 정도의 재사용 가능한 Logic을 구현할 수 있는데 이 경우 shared layout으로부터 component를 호출하여 navigation menu를 Rendering 함으로써 자연스럽게 Application과 통합할 수 있는 view component를 만들게 될 것입니다.
● Navigation View Component 생성
우선 project에 Components라는 folder를 만들고 Navigation Menu를 위한 NavigationMenuViewComponent.cs라는 file을 해당 folder에 추가하여 아래와 같이 구현합니다. 참고로 Components라는 folder는 관례적인 이름으로서 view component를 위한 folder임을 의미합니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Components
{
public class NavigationMenuViewComponent : ViewComponent
{
public string Invoke()
{
return "Navigation Menu";
}
}
}
예제에서 Invoke() Method는 Razor View에서 component가 사용될때 호출되며 Invoke() method에서 반환된 결과는 browser로 보내지는 HTML에 삽입될 것입니다. 현재 예제는 간단한 문자열을 반환하도록 되어 있고 실제로도 해당 문자열이 표시될 것입니다.
지금의 의도는 가능한한 모든 Page에 Category Navigation을 표시하는 것이므로 다른 View를 사용하기보다는 shared layout의 view component를 사용하고자 합니다. 따라서 아래와 같이 view component를 tag helper를 사용하여 _Layout.cshtml에 아래와 같이 적용하였습니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>My Web Application</title>
</head>
<body>
<div>
@RenderBody()
</div>
<div>
<vc:navigation-menu />
</div>
</body>
</html>
예제에서 사용된 <vc:navigation-menu />는 view component를 추가하는 element로서 class 명에서 ViewComponent를 생략하고 hyphen을 추가하는 형식으로 사용되었습니다. 따라서 navigation-menu는 곧 NavigationMenuViewComponent class를 뜻하는 것입니다. 여기까지 완성한 후 project를 실행해 보면 아래와 같은 결과를 얻을 수 있습니다.
● Category List 생성
HTML에 삽입된 문자열을 통해 생성한 view component가 제대로 작동하는 것을 확인했으므로 이제 view component를 수정하여 실제 Category를 표시할 수 있도록 할 것입니다. Category는 view component에서 문자열을 통해 표현하는 것도 가능할 테지만 view component는 문자열뿐만 아니라 view형식을 반환하는 것도 가능하므로 별도의 view를 만들어 view component에서 사용하면 될 것입니다. 이렇게 하면 view component를 통해 반환되는 view에서는 HTML생성을 위해 Razor 구문 등을 사용할 수도 있습니다.
view component는 Category명을 View Model로 지정하는 View를 반환하도록 아래와 같이 수정합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Migrations;
using MyWebApp.Models;
namespace MyWebApp.Components
{
public class NavigationMenuViewComponent : ViewComponent
{
private IMyDBRepository _repository;
public NavigationMenuViewComponent(IMyDBRepository repository)
{
_repository = repository;
}
public IViewComponentResult Invoke()
{
return View(_repository.Products.Select(x => x.Category).Distinct().OrderBy(x => x));
}
}
}
생성자에서는 IMyDBRepository형식의 매개변수를 구현했는데 ASP.NET Core가 view component의 instance를 생성할때 해당 매개변수의 값을 제공해야 함을 인식하고 Program.cs의 설정을 확인해 어떤 객체가 사용되어야 하는지를 결정하게 됩니다. 이를 통해 view component에서는 Repository를 통해 data의 접근이 가능하게 됩니다.
Invoke() Method는 repository를 통해 Product의 Category를 추출하여 View() Method의 매개변수로 넘기고 있는데 IViewComponentResult 객체를 반환하도록 하는 View()로 부터 넘겨진 Category를 표시할 default Razor partial view를 Rendering하게 됩니다.
● View 생성
Razor는 view component에 의해 선택된 view를 찾기위해 여러 가지 방식을 사용하는데 여기서 검색되는 view의 명칭과 위치는 Controller에서 사용되는 것과는 다르게 Views/Shared/Components folder하위의 view component class이름인 NavigationMenu folder에서 Default.cshtml file을 관례적으로 검색합니다. 따라서 해당 folder에 Default.cshtml file을 아래 내용으로 추가해 줍니다.
@model IEnumerable<string>
<div class="d-grid gap-2">
<a asp-action="Index" asp-controller="Home" asp-route-category="">전체</a>
@foreach (string category in Model ?? Enumerable.Empty<string>())
{
@:
<a asp-action="Index" asp-controller="Home" asp-route-category="@category" asp-route-currentPage="1">
@category
</a>
}
</div>
예제에서는 tag helper를 사용해 a element를 생성하면서 Category선택하는데 사용될 URL을 a elemnet의 href속성으로 처리하고 있습니다. 여기서 project를 실행하면 아래와 같이 category를 선택할 수 있는 link를 볼 수 있습니다.
● 선택된 Category 강조하기
Browser는 기본적으로 선택한 Link와 그렇지 않은 Link를 색상으로 구분해 주며 현재 표시된 Product List를 통해서도 현재 선택한 Category가 무엇인지를 판단할 수 있습니다. 하지만 사용자에게 좀 더 높은 접근성을 제공하기 위해 현재의 Category와 일치하는 Link를 강조하여 표시하는 기능을 추가하고자 합니다.
Controller와 View Component와 같은 ASP.NET Core component는 context 객체를 통해서 현재 요청한 정보를 확인할 수 있는데 대부분의 경우 context 객체는 Controller를 만들 때의 기반 class인 Controller Base Class처럼 Component를 생성하는 데 사용된 기반 class에 의존합니다.
ViewComponent base class또한 예외는 아니며 일련의 속성을 통해 context 객체로의 접근을 제공하고 있는데 그 속성들 중 하나로 RouteData라는 것을 사용할 수 있습니다. 이 속성은 현재 선택된 Category를 알아내기 위해 요청 Data로 접근할 수 있는데 선택된 Category는 또 다른 View Model을 생성하여 처리할 수 있으나 예제에서는 view bag기능을 통해 View Model 객체와 함께 해당 Data를 View로 전달할 것입니다.
public IViewComponentResult Invoke()
{
ViewBag.CurrentCategory = RouteData?.Values["category"];
return View(_repository.Products.Select(x => x.Category).Distinct().OrderBy(x => x));
}
NavigationMenu view Component의 Invoke() Method에서는 ViewBag을 통해 동적으로 CurrentCategory속성을 할당하고 현재 선택된 category값을 RouteData속성을 통해 반환된 context 객체를 사용하여 확인한 후 설정하고 있습니다. 여기서 ViewBag은 이들의 값을 설정하기 위해 새로운 속성을 정의하도록 해주는 객체입니다.
위의 과정을 통해 현재 선택된 Category를 View에서도 확인할 수 있게 되었으므로 Default.cshtml을 아래와 같이 수정하여 현재 선택된 Category가 좀 더 뚜렷하게 표현될 수 있도록 처리합니다.
@model IEnumerable<string>
<div class="d-grid gap-2">
<a asp-action="Index" asp-controller="Home" asp-route-category="">전체</a>
@foreach (string category in Model ?? Enumerable.Empty<string>())
{
@:
<a asp-action="Index" asp-controller="Home" asp-route-category="@category" asp-route-currentPage="1">
@if(category == ViewBag.CurrentCategory)
{
<b>@category</b>
}
else
{
@category
}
</a>
}
</div>
(4) Page Count 보정하기
현재 project의 실행결과를 보면 선택한 Category와는 관련없이 Page Count가 Repository에 있는 전체 Product를 대상으로 표시된다는 것을 알 수 있습니다. 실제 computer Category를 선택한 뒤 3 Page로 가면 빈 목록이 나오게 됨을 알 수 있는데 이는 computer category에 속한 Product가 3개의 page를 채우기에는 부족하다는 것을 의미하는 것입니다. 따라서 Category를 선택했을 때 선택한 Category에 속하는 Product의 수로 Page가 Count 될 수 있도록 아래와 같이 HomeController의 Index를 수정해 줍니다.
public IActionResult Index(string? category, int currentPage = 1)
{
var result = _repository.Products
.Where(p => p.Category == category || category == null)
.OrderBy(p => p.Id)
.Skip((currentPage - 1) * PageSize)
.Take(PageSize);
return View(
new ProductsListViewModel
{
Products = result,
PageInfo = new PageInfo
{
CurrentPage = currentPage,
ItemsPerPage = PageSize,
TotalItems = category == null ? _repository.Products.Count() : _repository.Products.Where(p => p.Category == category).Count()
},
CurrentCategory = category,
});
}
TotalItems에서 Product의 수를 지정할때 category가 지정되어 있지 않으면(null이면) 전체 Product의 Count를 가져오지만 그렇지 않으면 해당 category에 속하는 Product의 Count만을 산출하여 그 값을 TotalItems에 할당하고 있습니다.
2. Cart 기능
Project는 판매가능한 제품을 표시할 테지만 아직까지 사용자는 특정 제품을 구매할 수 없습니다. 제품을 판매하기 위한 Cart기능이 없기 때문이며 따라서 이 기능 또한 추가할 것입니다.
(1) Razor Page 사용을 위한 구성
이제까지 Project에서는 MVC Framework을 통해 Project의 기능을 구현했는데 조금 다른 방법으로 ASP.NET Core에서 지원하는 다른 application framework인 Razor Page를 사용해 Cart기능을 구현해 보려고 합니다. 이를 위해 Program.cs file을 수정하여 Project에서 Razor Page를 사용할 수 있도록 AddRazorPages() Method와 MapRazorPages() Method를 추가 설정합니다.
builder.Services.AddScoped<IMyDBRepository, MyDBRepository>();
builder.Services.AddRazorPages();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllerRoute("categoryPage", "{category}/Page{currentPage:int}", new { Controller = "Home", action = "Index" });
app.MapControllerRoute("page", "Page{currentPage:int}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("category", "{category}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapControllerRoute("default", "Products/Page{currentPage}", new { Controller = "Home", action = "Index", currentPage = 1 });
app.MapDefaultControllerRoute();
app.MapRazorPages();
MyData.InitData(app);
app.Run();
AddRazorPages() Method는 Razor Page에서 사용하는 Service를 설정하며 MapRazorPages Method는 URL routing system이 요청을 처리하는 끝점으로서 사용될 수 있도록 Razor Page를 등록합니다.
위와 같이 설정한 후 Project에서 Pages라는 이름의 folder를 생성하고 _ViewImports.cshtml이름의 file을 아래 내용으로 Pages folder에 추가합니다. 참고로 Pages folder는 Razor Page를 저장하기 위해 통상적으로 사용되는 이름의 folder입니다.
@namespace MyWebApp.Pages
@using Microsoft.AspNetCore.Mvc.RazorPages
@using MyWebApp.Models
@using MyWebApp.Classes
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
위 구현은 Razor Page에서 필요하게될 Namespace를 설정하는 것이며 Razor Page에서 자체의 Namespace를 지정하지 않고도 MyWebApp의 class를 사용할 수 있도록 합니다.
그다음 _ViewStart.cshtml이름의 Razor View시작 file을 Pages folder에 아래 내용으로 추가하여 project의 Razor Page가 _CartLayout이라는 이름의 Layout file을 기본적으로 사용할 수 있도록 처리합니다.
@{
Layout = "_CartLayout";
}
기본 Layout으로 _CartLayout을 설정했으므로 실제 Razor Page가 해당 file을 사용할 수 있도록 _CartLayout.cshtml이름의 file 또한 Pages folder에 추가해야 합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Computer Zone</title>
</head>
<body>
@RenderBody()
</body>
</html>
(2) Razor Page 생성하기
Razor Page사용을 위한 설정이 완료되었으므로 이제 Cart.cshtml이라는 Razor Page를 Pages에 추가하고 /Cart URL을 지정하여 정상적으로 Page가 표시되는지 확인합니다.
@page
<p>장바구니 영역</p>
/cart URL을 따로 Page와 Mapping되도록 지정하지 않아도 해당 처리는 자동으로 이루어지게 됩니다.
(3) Cart 추가
이제 '장바구니 추가' button을 만들어 Product를 Cart에 추가하는 기능을 구현해야 하는데 그전에 우선 Project의 Classes folder에 UrlExtension.cs이름의 file을 추가하고 아래 확장 method를 구현합니다.
namespace MyWebApp.Classes
{
public static class UrlExtension
{
public static string PathAndQuery(this HttpRequest request)
{
return request.QueryString.HasValue ? $"{request.Path}{request.QueryString}" : request.Path.ToString();
}
}
}
위에서 구현한 PathAndQuery() 확장 method는 ASP.NET Core가 Http요청을 표현하기 위해 사용하는 HttpRequest class하에서 작동하며 cart가 update 되고 나면 query string을 고려하여 browser가 반환하게 될 URL을 생성하고 Cart에서 다시 되돌아올 때 해당 URL이 사용될 수 있도록 할 것입니다.
위 확장 method가 완료되면 ProductList.cshtml을 수정하여 실제 Cart담기를 처리할 Button을 생성해 줍니다.
@model ProductsListViewModel
<table border="1">
<thead>
<tr>
<th>제품명</th>
<th>설명</th>
<th>단가</th>
<th> </th>
</tr>
</thead>
<tbody>
@foreach (var p in Model.Products ?? Enumerable.Empty<Product>())
{
<tr>
<td>@p.Name</td>
<td>@p.Description</td>
<td>@p.Price.ToString("#,###")</td>
<td>
<form id="form_@p.Id" asp-page="/Cart" method="post">
<input type="hidden" id="Product_@p.Id" name="ProductID" value="@p.Id" />
<input type="hidden" name="returnUrl" value="@ViewContext.HttpContext.Request.PathAndQuery()" />
<button type="submit">장바구니 추가</button>
</form>
</td>
</tr>
}
</tbody>
</table>
<div page-model="@Model?.PageInfo" page-action="Index" page-url-category="@Model?.CurrentCategory!"></div>
예제에서는 우선 Product의 ID와 위에서 설명한 URL을 포함하는 hidden field의 element를 가진 form element를 생성하고 form 내부에 submit type의 button을 추가하여 /Cart로 post전송이 이루어질 수 있도록 구현하였습니다.
(4) Session 사용하기
사용자의 Cart에 관한 data는 session state를 사용하여 저장할 것입니다. session state는 사용자에 의해 만들어 지는 일련의 요청과 관련된 data입니다. ASP.NET은 Memory를 포함하여 Session State를 저장하기 위한 다양한 방법을 제공하고 있는데 특히 Memoy를 활용하는 방법은 비교적 간단하기는 하지만 Application이 정지되거나 재시작되는 경우 관련 data가 손실될 수 있으니 주의가 필요합니다. session을 사용하기 위해서 Program.cs에 Service와 Middleware를 아래와 같이 추가해 줍니다.
builder.Services.AddScoped<IMyDBRepository, MyDBRepository>();
builder.Services.AddRazorPages();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
var app = builder.Build();
app.UseStaticFiles();
app.UseSession();
예제에서 사용된 AddDistributedMemoryCache() Method는 in-memory data 저장을 설정하며 AddSession() Method는 session data에 접근하는데 사용되는 service를 등록합니다. 그리고 마지막 UseSession() Method는 client로부터 session을 사용한 요청이 도달하면 이를 자동으로 session system과 연결될 수 있도록 합니다.
(5) Cart 기능 구현
위와 같은 준비가 완료되면 실제 Cart기능을 구현하기 위해서 Project의 Models folder에 Cart.cs이름의 file을 생성하고 아래와 같이 구현합니다.
namespace MyWebApp.Models
{
public class Cart
{
public List<CartItems> Items { get; set; } = new List<CartItems>();
public void AddItem(Product product, int quantity)
{
CartItems? line = Items.Where(p => p.Product.Id == product.Id).FirstOrDefault();
if (line == null)
{
Items.Add(new CartItems
{
Product = product,
Quantity = quantity
});
}
else
line.Quantity += quantity;
}
public void RemoveItem(Product product) => Items.RemoveAll(i => i.Product.Id == product.Id);
public decimal ComputeTotalValue() => Items.Sum(i => i.Product.Price * i.Quantity);
public void Clear() => Items.Clear();
}
public class CartItems
{
public int CartItemsID { get; set; }
public Product Product { get; set; } = new();
public int Quantity { get; set; }
}
}
Cart Class는 내부에 구매를 위해 선택한 Product와 수량을 표시하기 위한 또 다른 class인 CartItems라는 class를 정의하였습니다. 그리고 cart에 item추가를 위한 Method와 이미 추가된 item을 cart로부터 삭제하기 위한 Method, cart에 추가된 item의 금액을 계산하고 cart의 모든 item을 삭제하여 cart를 초기화시키는 Method를 포함하고 있습니다.
● Session State를 위한 확장 method 구현
ASP.NET Core에서 session state는 단지 int, string 그리고 byte[]형식의 값만을 저장할 수 있습니다. 하지만 여기서는 Cart객체의 저장을 필요로 하므로 ISession interface로 확장 method를 만들어 Cart 객체를 serialize하고 이를 다시 복원할 수 있는 session state data로의 접근을 제공하고자 합니다.
이를 위해 SessionExtensions.cs이름의 file을 Classes foler에 추가하고 아래와 같이 확장 method를 정의합니다.
using System.Text.Json;
namespace MyWebApp.Classes
{
public static class SessionExtensions
{
public static void SetJson(this ISession session, string key, object value)
{
session.SetString(key, JsonSerializer.Serialize(value));
}
public static T? GetJson<T>(this ISession session, string key)
{
var sessionData = session.GetString(key);
return sessionData == null ? default(T) : JsonSerializer.Deserialize<T>(sessionData);
}
}
}
예제의 Method는 객체를 JSON(JavaScript Object Notation)형식으로 serialize 하고 Generic을 통해 다시 지정한 형식으로 변환합니다. 이들 Method는 Cart객체를 저장하고 다시 가져오기 위한 목적으로 사용됩니다.
● Cart Razor Page 구현하기
Cart Razor Page는 사용자가 '장바구니 추가' button을 누르면 Browser로부터 HTTP POST 요청을 받게 될 것입니다. 이때 요청 form data를 사용하여 Database로 부터 product에 대한 객체를 가져오거나 session data로 저장되는 사용자의 cart를 update하는데 사용할 것입니다.
@page
@model CartModel
<h2>장바구니</h2>
<table>
<thead>
<tr>
<th>수량</th>
<th>제품</th>
<th>가격</th>
<th>금액</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model?.Cart?.Items ?? Enumerable.Empty<CartItems>())
{
<tr>
<td>@item.Quantity</td>
<td>@item.Product.Name</td>
<td>@item.Product.Price.ToString("c")</td>
<td>
@((item.Quantity * item.Product.Price).ToString("c"))
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3">Total:</td>
<td>
@Model?.Cart?.ComputeTotalValue().ToString("c")
</td>
</tr>
</tfoot>
</table>
<div>
<a href="@Model?.ReturnUrl">계속 쇼핑하기</a>
</div>
물론 Razor Page또한 HTML와 C# code를 단일 page에서 같이 사용할 수 있지만 되도록 이면 함께 추가된 Cart.cshtml.cs file을 활용하여 HTML과 C# code를 분리하기를 권장합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore.Migrations;
using MyWebApp.Classes;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
public class CartModel : PageModel
{
private IMyDBRepository _repository;
public CartModel(IMyDBRepository repository)
{
_repository = repository;
}
public Cart? Cart { get; set; }
public string ReturnUrl { get; set; } = "/";
public void OnGet(string returnUrl)
{
ReturnUrl = returnUrl ?? "/";
Cart = HttpContext.Session.GetJson<Cart>("cart") ?? new Cart();
}
public IActionResult OnPost(long ProductID, string returnUrl)
{
Product? product = _repository.Products.FirstOrDefault(p => p.Id == ProductID);
if (product != null)
{
Cart = HttpContext.Session.GetJson<Cart>("cart") ?? new Cart();
Cart.AddItem(product, 1);
HttpContext.Session.SetJson("cart", Cart);
}
return RedirectToPage(new { returnUrl = returnUrl });
}
}
}
위와 같이 Razor Page와 관련된 class를 흔히 page model class라고 하며 여기에서 각각 다른 HTTP 요청에 의해 호출되는 Method를 정의하고 예제에서와 같이 view가 rendering되기 전에 상태를 update 할 수 있게 합니다. 예제에서의 Page Model class는 CartModel이며 OnPost()라는 Method를 추가하여 HTTP POST요청에 호출될 수 있도록 하고 있습니다. OnPost() Method에서는 database로부터 Product의 정보를 확인한 뒤 session data로 부터 사용자의 Cart를 가져와 확인된 Product를 사용해 Cart를 Update 하고 있습니다. 그리고 Update 된 Cart를 저장한 뒤 Browser가 해당 Razor Page를 Redirect함으로서 GET 요청이 사용되도록 처리하고 있습니다.
Cart의 속성과 ReturnUrl값이 설정된 GET 요청은 OnGet() Method를 호출하게 되고 이후에 page의 Razor content section이 Rendering 됩니다. HTML content내부의 표현식은 view model 객체로서 PageModel을 통해 평가되는데 이는 ReturnUrl 및 Cart과 관련된 값이 표현식 안에서 접근될 수 있을 보여줍니다. 따라서 Razor Page에 의해 생성된 content는 사용자의 Cart에 추가된 Product를 자세히 열거할 수 있고 Product가 Cart에 추가될 때의 지점으로 돌아갈 수 있는 기능의 link를 같이 제공해 줄 수 있게 됩니다.
예제에서 구현된 Method는 parameter의 이름을 ProductList.cshtml view에 의해 생성된 HTML form에서 input element와 동일한 것으로 사용하고 있습니다. 이는 ASP.NET Core가 들어오는 form POST값을 parameter와 연결시키기 위한 것이며 이외 form과의 연결을 위한 다른 작업은 필요하지 않습니다. 이것은 model binding이라고 하는 기능때문이며 개발과정을 간소화하는데 도움이 될 수 있습니다.
project를 실행하면 '장바구니 추가'라는 button과 함께 Product의 List를 볼 수 있고
이어서 '장바구니 추가'를 click하게 되면 장바구니 Page에서 해당 제품과 단가를 확인할 수 있습니다.
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] Shopping mall project - 관리자기능 (0) | 2022.09.01 |
---|---|
[ASP.NET Core] Shopping mall project - 주문완료하기 (0) | 2022.08.26 |
[ASP.NET Core] Shopping mall project 시작하기 (0) | 2022.08.16 |
[ASP.NET Core] 초간단 Application 만들어 보기 (0) | 2022.08.03 |
[ASP.NET Core] 시작하기 (0) | 2022.07.29 |