고객이 Product를 주문하기까지의 기능은 모두 완료되었습니다. 그러나 아직 한 가지 더 해결해야 할 문제가 남았는데 그것은 관리자가 주문과 Product를 어떻게 관리할 수 있냐 하는 것입니다. 이를 해결하기 위해 관리자 기능을 추가할 텐데 이번 예제에서는 Blazor를 사용하여 해당 기능을 추가할 것입니다. Blazor는 client-side JavaScript code와 ASP.NET Core에 의해서 실행되는 server-side code를 결합하는 것으로 영속적 HTTP 연결(persistent HTTP connection)을 통해 연결됩니다. Blazor에 관해서는 추후에 상세히 다루겠지만 분명히 알아둬야 할 것은 Blazor가 모든 Project에 적합한 것은 아니라는 것입니다.
Blazor는 2가지가 존재하는데 하나는 ASP.NET Core platform의 일부로서 지원되는 Blazor Server로 예제에서 사용하고자 하는 것이며 다른 하나는 Blazor Web Assembly로 browser에서 전적으로 실행되는 형태로 아직까지는 실험단계에 있는 기술입니다.
1. Blazor Server 준비하기
우선 Blazor를 위한 middleware와 service를 사용하기 위해 Program.cs에서 아래와 같이 service를 추가합니다.
..생략
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddServerSideBlazor();
..생략
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/admin/{*catchall}", "/Admin/Index");
..생략
AddServerSideBlazor method는 Blazor가 사용하는 service를 생성하며 MapBlazorHub method는 Blazor middleware component를 등록합니다. 마지막으로 MapFallbackToPage는 routing system을 개선하여 Blazor가 나머지 application과 원활하게 작동하는 설정하는 것입니다.
(1) import file 생성
Blazor는 Blazor를 사용하기 위해 필요한 namespace를 지정하는 자체 import file이 필요합니다. 이를 위해 Pages > Admin folder를 만들고 _Imports.razor이름의 file을 아래와 같이 추가합니다.(Visual Studio를 사용한다면 file을 생성하기 위해 RazorCompo nents template을 사용할 수 있습니다.)
Blazor file은 기본규칙상 Pages foler안에 위치하지만 원한다면 Project의 어느 위치에서든 정의될 수 있습니다.
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.EntityFrameworkCore
@using CompuMallStore.Models
처음 4개의 @using 표현식에서 사용된 namespace만이 직접적으로 Blazor에 필요한 것이며 나머지 2개는 예제에서 Entity Framework Core와 Models namespace를 사용하기 위한 것입니다.
(2) Startup Razor Page 생성
Blazor는 초기 content를 제공하기 위해 Razor Page에 의존하는데 content에는 Server와 연결하고 Blazor HTML을 render 하는 JavaScript를 포함합니다. Index.cshtml이름의 file을 Pages > Admin folder에 아래와 같이 추가합니다.
@page "/admin"
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
<title>CompuMallStore Admin</title>
<base href="/" />
</head>
<body>
<component type="typeof(Routed)" render-mode="Server" />
<script src="/_framework/blazor.server.js"></script>
</body>
</html>
예제에서 component요소는 Razor Page에서 Razor Component를 삽입하기 위해 사용됩니다. Razor Component는 Razor Page와 이름에서 혼란이 올 수 있지만 엄밀히 다른 것이며 Blazor를 만들어 가는 block에 해당합니다. Razor Page에 적용된 component는 Routed로 명명되었으며 현재 에러가 발생할 수 있지만 곧 이 문제를 해결할 것입니다. Razor Page에서는 또한 browser가 어떤 JavaScript file을 load 해야 할지를 말해주는 script요소를 포함하고 있는데 이는 Blazor Server에서 사용됩니다. 해당 file에 대한 요청은 Blazor Server middleware에 의해 가로채어 처리하므로 명시적으로 해당 file을 추가하는 질은 업로드 해야 합니다.
(3) routing과 layout component 생성
Admin foler에 Routed.razor이름의 file을 아래와 같이 추가합니다.
<Router AppAssembly="typeof(Program).Assembly">
<Found>
<RouteView RouteData="@context" DefaultLayout="typeof(AdminLayout)" />
</Found>
<NotFound>
<h3>Not Route Found</h3>
</NotFound>
</Router>
해당 component는 browser의 현재 URL을 사용해 사용자에게 표시할 Razor component를 찾는데, 만약 일치하는 component를 찾지 못하면 NoFound 요소의 내용이 대신 표시됩니다.
Blazor는 자체 layout system을 갖고 있습니다. 관리 도구를 위한 layout을 만들기 위해 Admin folder에 AdminLayout.razor이름의 file을 아래와 같이 추가합니다.
@inherits LayoutComponentBase
<style>
.column {
float: left;
width: 20%;
}
.row:after {
content: "";
display: table;
clear: both;
}
</style>
<div>
<span>CompuMall Administration</span>
</div>
<div>
<div class="row">
<div class="column">
<div class="row">
<NavLink href="/admin/products" Match="NavLinkMatch.Prefix">
Products
</NavLink>
</div>
<div class="row">
<NavLink href="/admin/orders" Match="NavLinkMatch.Prefix">
Orders
</NavLink>
</div>
</div>
<div class="column">
@Body
</div>
</div>
</div>
Blazor는 HTML을 생성하기 위해 Razor 문을 사용하지만 예제에서는 자체 지시자와 기능을 사용하였습니다. 예제는 NavLink요소를 통해 만들어진 Products와 Orders라는 2개의 button을 가지며 2개의 화면으로 나누어진 결과를 표시할 것입니다. 또한 새로운 HTTP요청이 없이도 URL을 변경하는 내장 Razor component를 적용하여 Blazor가 사용자와의 상호작용에서 Application의 상태를 잃지 않고도 응답을 수행할 수 있게 합니다.
(4) Razor Component 생성
초기 설정을 완료하기 위해 비록 간단한 문자열만을 포함하고 있지만 아래와 같이 관리도구를 제공할 component를 추가할 것입니다. Pages > Admin folder에 Products.razor이름의 Razor component file을 아래와 같이 추가합니다.
@page "/admin/products"
@page "/admin"
<h4>Products component</h4>
@page 지시자는 component가 표시될 URL을 지정하는 것으로 예제에서는 /admin/products와 /admin 2개를 지정하고 있습니다. 다음으로 같은 folder에 Orders.razor이름의 Razor Component를 아래와 같이 추가합니다.
@page "/admin/orders"
<h4>Orders component</h4>
(5) Blazor 설정 확인
Blazor가 정상적으로 작동함을 확인하기 위해 Project를 실행하고 /admin으로 URL을 요청합니다. 이 요청은 Pages > Admin folder에 있으며 browser로 보낼 content에 Blazor JavaScript file을 포함하고 있는 Index Razor Page에 의해 처리될 것입니다. JavaScript code는 영속적 HTTP 연결을 열고 ASP.NET Core server와 연결한 다음 초기 Blazor content를 render 하여 다음과 같은 결과를 표시할 것입니다.
단위 TEST
2024년 04월 현재까지 Razor Component를 Test할 수 있는 별도의 test 도구는 존재하지 않습니다.
위 상태에서 Orders link를 click하면 Orders Razor Component에 의해 생성된 content가 표시될 것입니다. 이때 browser의 content가 바뀌는 순간에 주목하시기 바랍니다. 지금까지 사용했던 다른 ASP.NET Core application framework와 달리 새로운 HTTP요청을 보내지 않고도 browser에 의해 URL이 바뀌면서 다른 content가 표시됩니다.
2. 주문관리
위에서 Blazor가 잘 작동하고 있음을 확인했으면 본격적으로 관리자기능을 구현해 볼 것입니다. 고객으로부터 주문을 받고 주문상세를 database에 저장하는 것까지 완료했으므로 들어온 주문내역을 확인하고 배송했음을 표시하는 관리자 기능을 추가할 것입니다.
(1) Model 변경하기
첫번째로 필요한 건 data model을 변경하여 배송된 주문을 구별하도록 하는 것입니다. 이를 위해 Models folder의 Order.cs에서 정의된 Order class에 다음과 같이 새로운 속성을 추가합니다.
..생략
[Required(ErrorMessage = "Please enter a address")]
public string? Address { get; set; }
[BindNever]
public bool Shipped { get; set; }
ASP.NET Core개발에서 다른 기능을 지원하기 위해 수시로 data model을 확장하고 적용하는 것은 일반적인 방식입니다. 이상적으로는 Project를 시작할 때 완벽하게 data model을 정의하고 이를 중심으로 application을 build 할 수 있지만 이런 경우는 아주 간단한 Project에서만 가능한 일이며 실제로는 Application에 대한 요구사항의 변화와 목표에 따라 반복적인 개발이 진행될 수 있습니다.
이때 Entity Framework Core migration은 직접 SQL을 작성함으로써 Database schema와 model class를 수동적으로 동기화하지 않고도 쉽게 이러한 개발과정을 지원할 수 있습니다. Order class에 추가한 Shipped속성을 database에 반영하기 위해 명령 prompt에서 다음 명령을 내려줍니다.
dotnet ef migrations add ShippedOrders |
migration은 Application이 실행되고 SeedData class가 Entity Framework Core에서 제공하는 Migrate method를 호출함으로써 자동적으로 적용될 것입니다.
(2) 주문내역 표시하기
이번 예제에서는 2개의 table을 사용해 주문내역을 표시할텐데 하나는 배송 전 대기상태인 주문내역을 표시하고 다른 하나는 배송된 주문내역을 표시할 것입니다. 이때 각 주문에 link를 추가하여 현재 주문상태를 변경할 수 있도록 할 것입니다. (실제 주문처리는 database의 field하나를 바꾸는 것 이상의 훨씬 복잡한 처리를 필요로 하므로 사실 이러한 방식이 일반적이지는 않지만...)
Code와 content의 중복을 피하기 위해 Razor Component를 만들어 table을 표시할 것입니다. 이를 위해 Pages > Admin folder에 OrderTable.razor이름의 Razor Component를 아래와 같이 추가합니다.
<table border="1">
<thead>
<tr><th colspan="4">@TableTitle</th></tr>
</thead>
<tbody>
@if (Orders?.Count() > 0) {
@foreach (Order o in Orders) {
<tr>
<td>@o.Name</td>
<td>@o.Address</td>
<td>Quantity</td>
<td>
<button @onclick="@(e => OrderSelected.InvokeAsync(o.OrderID))">@ButtonLabel</button>
</td>
</tr>
@foreach (CartItem line in o.Items) {
<tr>
<td>@line.Product.Name</td>
<td></td>
<td>@line.Quantity</td>
<td></td>
</tr>
}
}
} else {
<tr>
<td colspan="4">No Orders</td>
</tr>
}
</tbody>
</table>
@code {
[Parameter]
public string TableTitle { get; set; } = "Orders";
[Parameter]
public IEnumerable<Order> Orders { get; set; } = Enumerable.Empty<Order>();
[Parameter]
public string ButtonLabel { get; set; } = "Ship";
[Parameter]
public EventCallback<int> OrderSelected { get; set; }
}
Razor Component는 이름에서 추정할 수 있듯이 주석이 달린 HTML요소에만 Razor접근방식에 의존합니다. 또한 component의 view부분은 @code영역에서의 문에 의해 지원되는데 예제에서는 4개의 속성이 정의된 @code영역에 Parameter라는 attribute를 적용하였습니다. 이로서 해당 속성의 값은 이제 곧 만들 상위 component에 의해 runtime에서 제공될 수 있습니다. parameter에 제공된 값은 component의 view영역에서 일련의 Order개체에 대한 상세를 표시하는 데 사용됩니다.
Blazor는 Razor문으로 표현식을 추가하는데 예제에서 component의 view영역에서는 @onclick attribute를 가진 button요소를 포함하고 있습니다. 이것은 Blazor가 사용자가 button을 click 했을 때 어떻게 반응해야 할지를 지정하는 것으로 예제의 경우 Razor가 OrderSelected속성의 InvokeAsync method를 호출하도록 하고 있으며 table이 Blazor application과 통신하는 방식이기도 합니다.
Blazor에 관해서는 추후에 상세히 알아볼 것입니다. 따라서 Razor Component가 지금당장 이해하기 어렵더라도 걱정할 필요는 없습니다. 이번 글의 목적은 ASP.NET Core를 통한 전체적인 개발과정을 보여주는 것으로 개별적으로 사용된 기능에 대해서는 세부적인 이해를 필요로 하지 않습니다.
다음으로 database에서 Order data를 가져올 component를 만들고 OrderTable component를 사용해 사용자에게 해당 component를 표시할 것입니다. Pages > Admin folder에 있는 Orders.razor component를 아래와 같이 변경합니다.
@page "/admin/orders"
@inherits OwningComponentBase<IOrderRepository>
<OrderTable TableTitle="Unshipped Orders" Orders="UnshippedOrders" ButtonLabel="Ship" OrderSelected="ShipOrder" />
<OrderTable TableTitle="Shipped Orders" Orders="ShippedOrders" ButtonLabel="Reset" OrderSelected="ResetOrder" />
<button @onclick="@(e => UpdateData())">Refresh</button>
@code {
public IOrderRepository Repository => Service;
public IEnumerable<Order> AllOrders { get; set; } = Enumerable.Empty<Order>();
public IEnumerable<Order> UnshippedOrders { get; set; } = Enumerable.Empty<Order>();
public IEnumerable<Order> ShippedOrders { get; set; } = Enumerable.Empty<Order>();
protected async override Task OnInitializedAsync()
{
await UpdateData();
}
public async Task UpdateData()
{
AllOrders = await Repository.Orders.ToListAsync();
UnshippedOrders = AllOrders.Where(o => !o.Shipped);
ShippedOrders = AllOrders.Where(o => o.Shipped);
}
public void ShipOrder(int id) => UpdateOrder(id, true);
public void ResetOrder(int id) => UpdateOrder(id, false);
private void UpdateOrder(int id, bool shipValue) {
Order? o = Repository.Orders.FirstOrDefault(o => o.OrderID == id);
if (o != null) {
o.Shipped = shipValue;
Repository.SaveOrder(o);
}
}
}
Blazor Component는 예제 application에서 사용자 interface에 사용된 다른 application framework building block과는 차이가 있습니다. 개별요청을 처리하는 대신 component는 수명이 길고 장기간에 걸쳐 다중 사용자의 상호작용을 처리할 수 있습니다. 이는 다른 개발 style을 필요로 하는데 특히 Entity Framework Core를 사용해 data를 처리할 때 더욱 그렇습니다. @inherits 표현식은 해당 component가 자체 repository개체를 가져올 수 있도록 하는 것으로 같은 사용자에게 표시된 다른 component에서 수행된 것과 동작을 분리하기 위한 것입니다. 무엇보다 가장 큰 건 동일한 질의가 database에 반복적으로 들어가는 것을 방지하는 것인데 추후에 자세히 알아보겠지만 이는 Blazor에서 심각한 문제를 유발할 수 있으므로 중요합니다. 이와 같은 방법으로 인해 component가 초기화될 때 또는 Blazor가 OnInitializedAsync method를 호출할 때, 사용자가 Refresh button을 click 할 때만 repository가 사용됩니다.
예제에서는 사용자에게 data를 표시하기 위해 HTML요소에 OrderTable component를 아래와 같이 적용하였습니다.
<OrderTable TableTitle="Unshipped Orders" Orders="UnshippedOrders" ButtonLabel="Ship" OrderSelected="ShipOrder" />
OrderTable attribute에 할당된 값은 Parameter attribute가 적용된 속성에 값을 설정하는 데 사용됩니다. 이러한 방식을 통해 단일 component는 code와 content를 중복해 사용하지 않고도 2개의 다른 data집합이 표시되도록 구성할 수 있습니다.
ShipOrder와 ResetOrder method는 OrderSelected attribute에 대한 값으로 사용되는데 사용자가 OrderTable component에서 표시된 button을 click 함으로써 호출되어 repository를 통해 database의 data를 update 하게 됩니다.
예제를 실행하고 주문을 생성한 뒤 /admin/orders URL을 요청하여 Unshipped Orders table에 표시된 주문상황을 확인합니다.
여기서 Ship button을 누르게 되면 주문이 update 되어 Shipped Orders table로 이동하게 됩니다.
3. Category 관리 추가
예제에서는 사용자에게 Category관리기능을 제공하기 위해 item의 list를 보여주는 화면과 item을 수정하기 위한 편집화면을 제공할 것입니다. 이를 통해 사용자는 item을 생성하고, 일고, 수정하고, 삭제하는 CRUD동작을 수행할 수 있을 것입니다.
CRUD는 매우 흔한 기능으로서 Visual Studio scaffolding은 CRUD Controller 혹은 Razor page 생성을 위한 scenario를 포함하고 있습니다. 하지만 Visual Studio scaffolding을 사용하기보다는 이들 기능을 직접적으로 어떻게 구현할 수 있는지 알아보는 것을 더 추천하며 이를 위해 곧 관련된 내용들에 대해서 자세히 살펴볼 것입니다.
(1) Repository 확장
우선 repository에 Product 개체를 저장, 변경, 삭제하기 위한 기능을 추가해야 합니다. 이를 위해 Models folder의 IStoreRepository.cs file을 아래와 같이 변경합니다.
namespace CompuMallStore.Models;
public interface IStoreRepository
{
IQueryable<Product> Products { get; }
void SaveProduct(Product p);
void CreateProduct(Product p);
void DeleteProduct(Product p);
}
그다음 Models folder의 EFStoreRepository.cs파일에 정의된 Entity Framework Core repository class에서 위 interface의 method를 구현합니다.
namespace CompuMallStore.Models;
public class EFStoreRepository : IStoreRepository
{
private StoreDbContext context;
public EFStoreRepository(StoreDbContext ctx)
{
context = ctx;
}
public IQueryable<Product> Products => context.Products;
public void CreateProduct(Product p)
{
context.Add(p);
context.SaveChanges();
}
public void DeleteProduct(Product p)
{
context.Remove(p);
context.SaveChanges();
}
public void SaveProduct(Product p)
{
context.SaveChanges();
}
}
(2) Model data에 validation attribute 적용하기
사용자가 Product개체를 추가, 편집할 때 제공한 값에 대해서 유효성을 검증하기 위해 Product data model class에 아래와 같이 validation attribute를 추가합니다.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CompuMallStore.Models;
public class Product
{
public long? ProductID { get; set; }
[Required(ErrorMessage = "Please enter a product name")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Please enter a description")]
public string Description { get; set; } = string.Empty;
[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")]
[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }
[Required(ErrorMessage = "Please specify a category")]
public string Category { get; set; } = string.Empty;
}
Blazor는 다른 ASP.NET Core와 동일한 유효성 검사 접근 방식을 사용하지만 Razor Component에서는 다른 방식으로 적용됩니다.
(3) List component 생성
사용자에게 Product에 대한 table과 세부 사항을 확인하고 편집할 수 있는 link를 제공하기 위해 Products.razor file을 아래와 같이 변경합니다.
@page "/admin/products"
@page "/admin"
@inherits OwningComponentBase<IStoreRepository>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<td />
</tr>
</thead>
<tbody>
@if (ProductData?.Count() > 0) {
@foreach (Product p in ProductData) {
<tr>
<td>@p.ProductID</td>
<td>@p.Name</td>
<td>@p.Category</td>
<td>@p.Price.ToString("c")</td>
<td>
<NavLink href="@GetDetailsUrl(p.ProductID ?? 0)">
Details
</NavLink>
<NavLink href="@GetEditUrl(p.ProductID ?? 0)">
Edit
</NavLink>
</td>
</tr>
}
}
else {
<tr>
<td colspan="5">No Products</td>
</tr>
}
</tbody>
</table>
<NavLink href="/admin/products/create">Create</NavLink>
@code
{
public IStoreRepository Repository => Service;
public IEnumerable<Product> ProductData { get; set; } = Enumerable.Empty<Product>();
protected async override Task OnInitializedAsync()
{
await UpdateData();
}
public async Task UpdateData()
{
ProductData = await Repository.Products.ToListAsync();
}
public string GetDetailsUrl(long id) => $"/admin/products/details/{id}";
public string GetEditUrl(long id) => $"/admin/products/edit/{id}";
}
예제 component는 각 table의 행마다 상세 view와 편집을 제공할 component로 이동하는 NavLink component와 함께 repository로부터 Product개체를 표현하고 있습니다. 또한 새로운 Product 개체를 만들고 database에 저장하기 위해 해당 component로 이동할 수 있는 button 역시 제공하고 있습니다. 예제를 실행하고 /admin/products로 URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다. 물론 관련된 component를 아직 만들지 않았기에 Products component에서 표시된 어떠한 button도 작동하지는 않을 것입니다.
(4) 상세 Component 생성
상세 Component의 역할은 Product 개체에 대한 모든 field를 나타내는 것입니다. Details.razor이름의 Razor component를 Pages > Admin folder에 아래와 같이 추가합니다.
@page "/admin/products/details/{id:long}"
@inherits OwningComponentBase<IStoreRepository>
<h3>Details</h3>
<table>
<tbody>
<tr><th>ID</th><td>@Product?.ProductID</td></tr>
<tr><th>Name</th><td>@Product?.Name</td></tr>
<tr><th>Description</th><td>@Product?.Description</td></tr>
<tr><th>Category</th><td>@Product?.Category</td></tr>
<tr><th>Price</th><td>@Product?.Price.ToString("C")</td></tr>
</tbody>
</table>
<NavLink href="@EditUrl">Edit</NavLink>
<NavLink href="/admin/products">Back</NavLink>
@code
{
[Inject]
public IStoreRepository? Repository { get; set; }
[Parameter]
public long Id { get; set; }
public Product? Product { get; set; }
protected override void OnParametersSet()
{
Product = Repository?.Products.FirstOrDefault(p => p.ProductID == Id);
}
public string EditUrl => $"/admin/products/edit/{Product?.ProductID}";
}
예제에서는 Inject attribute를 사용하여 IStoreRepository interface의 구현이 필요한 field임을 나타내고 있습니다. 이는 Blazor가 Application의 service로 접근하기 위한 방법 중 하나입니다. ID속성의 값은 component로 이동하는 데 사용된 URL에서 가져오게 되며 Database에서 Product개체를 가져오기 위해 사용됩니다. 상세 view를 확인하기 위해 Project를 실행하고 /admin/products URL을 요청한 뒤 details button을 click 해 봅니다.
(5) 수정 Component 생성
Data에 대한 편집과 생성은 같은 component를 통해 수행할 것입니다. 이를 위해 Pages > Admin folder에 Editor.razor이름의 component를 아래와 같이 추가합니다.
@page "/admin/products/edit/{id:long}"
@page "/admin/products/create"
@inherits OwningComponentBase<IStoreRepository>
<style>
div.validation-message { color: rgb(220, 53, 69); font-weight: 500 }
</style>
<h3>
@TitleText a Product
</h3>
<EditForm Model="Product" OnValidSubmit="SaveProduct">
<DataAnnotationsValidator />
@if(Product.ProductID.HasValue && Product.ProductID.Value != 0)
{
<div>
<label>ID</label>
<input disabled value="@Product.ProductID" />
</div>
}
<div>
<label>Name</label>
<ValidationMessage For="@(() => Product.Name)" />
<InputText @bind-Value="Product.Name" />
</div>
<div>
<label>Description</label>
<ValidationMessage For="@(() => Product.Description)" />
<InputText @bind-Value="Product.Description" />
</div>
<div>
<label>Category</label>
<ValidationMessage For="@(() => Product.Category)" />
<InputText @bind-Value="Product.Category" />
</div>
<div>
<label>Price</label>
<ValidationMessage For="@(() => Product.Price)" />
<InputNumber @bind-Value="Product.Price" />
</div>
<div>
<button type="submit">Save</button>
<NavLink href="/admin/products">Cancel</NavLink>
</div>
</EditForm>
@code {
public IStoreRepository Repository => Service;
[Inject]
public NavigationManager? NavManager { get; set; }
[Parameter]
public long Id { get; set; } = 0;
public Product Product { get; set; } = new Product();
protected override void OnParametersSet()
{
if (Id != 0)
Product = Repository.Products.FirstOrDefault(p => p.ProductID == Id) ?? new();
}
public void SaveProduct()
{
if (Id == 0)
Repository.CreateProduct(Product);
else
Repository.SaveProduct(Product);
NavManager?.NavigateTo("/admin/products");
}
public string ThemeColor => Id == 0 ? "primary" : "warning";
public string TitleText => Id == 0 ? "Create" : "Edit";
}
Blazor는 form요소를 표시하고 유효성검증을 수행하는 일련의 기본 Razor Component를 제공하고 있습니다. Browser는 Blazor Component안에서 HTTP Post요청을 통해 data를 전송할 수 없기 때문에 이것은 매우 중요합니다. EditForm component는 Blazor친화적인 form을 render 하는 데 사용되며 InputText와 InputNumber component는 문자열/숫자값을 수용하고 사용자가 변경할 때 Model속성을 자동적으로 update 하는 input요소를 render 합니다.
Data에 대한 유효성검증은 이러한 내장 component로 통합되었으며 EditForm component상에서 OnValidSubmit속성은 form에 입력된 data가 validation attribute에서 정의된 규칙을 통과할 때 호출되는 method를 지정하는 데 사용됩니다.
Blazor는 또한 새로운 HTTP요청이 없이도 component사이를 program적으로 이동할 수 있게 하는 NavigationManager class를 제공합니다. Editor component는 이 component를 service로 가져와 database를 update 한 이후 Products component를 반환하는 데 사용하고 있습니다.
편집기능을 확인해 보기 위해 Project를 실행하고 /admin으로 URL을 요청한 다음 Create button을 click 합니다.
이 상태에서 어떠한 것도 입력하지 않고 Save button을 click 하여 Blazor가 자동적으로 생성하는 validation error를 확인합니다.
이제 필요한 모든 field를 채운 뒤 다시 'Save' button을 눌러 입력한 결과를 확인합니다.
이번에는 기존의 Product에서 Edit를 눌러 기존의 data를 변경해 봅니다.
Save button을 눌러 잘 저장되는지 확인합니다.
(6) Product 삭제
개체를 삭제하는 작업은 비교적 수월하게 진행할 수 있습니다. 특정 Product의 삭제를 위해 Products component에 아래와 같이 삭제 button과 관련 method를 추가해 줍니다.
...생략
<td>
<NavLink href="@GetDetailsUrl(p.ProductID ?? 0)">
Details
</NavLink>
<NavLink href="@GetEditUrl(p.ProductID ?? 0)">
Edit
</NavLink>
<button @onclick="@(e => DeleteProduct(p))">Delete</button>
</td>
...생략
public async Task UpdateData()
{
ProductData = await Repository.Products.ToListAsync();
}
public async Task DeleteProduct(Product p)
{
Repository.DeleteProduct(p);
await UpdateData();
}
예제에서 button요소는 @onclick attribute를 설정하여 DeleteProduct method를 호출하도록 하였습니다. /admin/products URL을 요청하고 특정 Product에 해당하는 Delete button을 click 하여 database로부터 해당 개체가 삭제되고 변경사항이 제대로 표시되는지 확인합니다.
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] - 11. ASP.NET Core platform (0) | 2024.05.15 |
---|---|
[ASP.NET Core] - 10. Shopping mall project 만들기 - 5 (2nd) (0) | 2024.05.04 |
[ASP.NET Core] - 8. Shopping mall project 만들기 - 3 (2nd) (0) | 2024.04.19 |
[ASP.NET Core] - 7. Shopping mall project 만들기 - 2 (2nd) (0) | 2024.04.15 |
[ASP.NET Core] - 6. Shopping mall project 만들기 - 1 (2nd) (2) | 2024.04.05 |