ASP.NET Core - [Blazor] 2. Blazor Server
Blazor는 web application에 client-side 상호작용을 추가하는 것으로 이번 글에서는 Blazor의 2가지 종류 중 Blazor Server에 관해 알아보고자 합니다. 대체로 Blazor server의 작동방식과 예상가능한 문제에 대해 어떻게 대처할 수 있을지에 대한 전반적인 내용을 확인할 것이며 ASP.NET Core application에서 Blzor Server를 사용하기 위한 구성방법과 Blazor Server Project를 위한 부품의 역할을 하는 것으로 Razor component사용 시 가능한 기본적인 기능에 대해서도 살펴보고자 합니다.
1. Project 준비
필요한 Project는 이미 아래 글에서 만들어 보았으며 여기에서 더 바뀌는 것은 없습니다.
[.NET/ASP.NET] - ASP.NET Core - [Blazor] 1. 예제 project 만들기
2. Blazor Server의 이해
위 예제를 실행하여 'Manufacturer'를 선택하고 'Select' button을 누르면 application 뒤편에서는 무슨 일이 일어날지 상상해 보시기 바랍니다. Browser는 controller나 razor page를 사용하는 것에 따라 action method나 handler method에서 전달받는 form을 submit 하는 HTTP GET 요청을 전송하게 되며 action이나 handler에서는 선택한 것을 반영하는 새로운 HTML문서를 browser로 전송하는 view를 render 하게 됩니다.
위 동작에 관한 주기는 효과적이지만 반면 비효휼적일 수도 있습니다. submit button을 click 할 때마다 browser는 새로운 HTTP요청을 ASP.NET Core에 전송합니다. 각 요청은 요청과 browser가 받을 응답의 유형을 서술하는 완전한 HTTP header설정을 포함해야 하며 응답에서 server는 응답을 서술하는 HTTP header를 포함하며 browser가 표시할 완전한 HTML문서를 포함합니다.
예제 application에서 전송하는 data의 크기는 대략 3kb정도 되는데 거의 대부분은 요청 간 중복되는 것들입니다. browser는 단지 Server에게 어떤 'Manufacturer'가 선택되었는지를 알려주기만 하면 되는 것이며 Server는 그저 Table의 row를 강조하여 표시하면 될 뿐입니다. 그러나 각 HTTP요청은 독립적이며 browser 역시 매번 완전한 HTML문서를 읽어 들어야 하는 것입니다. 모든 상호작용이 동일하다는 근본적인 문제는 비슷한 요청을 보내면서 거의 비슷하게 반환되는 HTML문서전체를 다루어야 한다는 것입니다.
하지만 Blazor는 다른 접근법을 취하고 있습니다. Javascript library가 browser로 전송되는 HTML문서안에 포함되고 Javascript code가 실행될 때 server로의 HTTP연결을 다시 열어두어 사용자의 상호작용을 준비하게 됩니다. 예를 들어 사용자가 select요소에서 값을 선택하게 되면 선택한 세부사항이 server로 전송되고 기존 HTML에 적용할 변경사항만을 응답받는 것입니다.
persistent HTTP connection은 지연을 최소화 하면서 변경된 것만을 응답함으로써 browser와 server 간에 전송되는 data의 양을 감소시키게 됩니다.
(1) Blazor Server의 장점
Blazor의 가장 큰 매력은 C#으로 작성된 Razor Page에 기반한다는 것입니다. 이 것은 Angular나 React와 같은 새로운 framework를 배우지 않고 또한 TypeScript나 JavaScript와 같은 새로운 언어를 배우지 않고도 높은 수준의 효율성과 반응성을 가진 Application을 개발할 수 있게 합니다. Blazor는 ASP.NET Core의 나머지 부분과 잘 통합되었으며 이전에 설명된 기능을 기반으로 하므로 쉽게 사용할 수 있습니다.(특히 어지러울 정도로 급격한 학습 곡선을 가진 Angular와 같은 framework와 비교해서는 더욱 그렇습니다.)
(2) Blazor Server의 단점
Blazor는 지속적 HTTP 연결을 구축하고 관리하기 위해 최신의 Browser를 필요로 하며 이러한 연결성 때문에 Blazor를 사용하는 application은 연결을 잃게 되면 작동을 중지하게 되므로 offline사용에는 적합하지 않습니다. 또한 동작방식에 대한 특징 때문에 연결성을 신뢰할 수 없고 연결 속도가 느릴 수 있습니다. 이러한 문제는 WebAssembly를 통해 극복할 수 있지만 나름대로의 한계점을 여전히 가지고 있습니다.
(3) Blazor와 Angular/React/Vue.js의 선택
Blazor와 JavaScript framework중에서의 선택은 개발자의 경험과 사용자의 예상되는 연결성에 의해 결정될 수 있습니다. JavaScript에 대한 경험이 없거나 JavaScript framework를 사용해 본 적이 없다면 Blazor를 선택할 수 있지만 안정된 연결성과 최신의 Browser가 사용되어야 함을 감안해야 합니다. 따라서 Blazor는 network 품질을 사전에 결정할 수 있는 LOB(Line-of-business) Application에 적합합니다.
아니면 JavaScript에 대한 경험이 있고 공개 Application을 만드는 경우라면 이때는 network 품질이나 사용되는 Browser를 사전에 판단할 수 없으므로 대신 JavaScript framework를 사용할 수 있습니다.(어떤 framework를 선택하느냐는 중요한 문제가 아닙니다. Angular나 React, Vue.js는 모두 훌륭한 framework이며 각각의 framework를 사용해 간단한 app을 만들어 보고 이 중에서 당신에게 가장 적합하다고 판단되는 개발 model을 가진 framework를 선택하면 됩니다.)
공개 application을 만들지만 JavaScript에 대한 경험이 없다면 2가 선택사항이 생길 수 있습니다. 가장 안전한 option은 지금까지 설명된 ASP.NET Core 기능의 사용을 고수하고 이것이 가져오는 비효휼성을 받아들이는 것입니다. 그다지 나쁜 선택은 아니며 여전히 고품질의 application을 개발할 수 있습니다. 좀 더 다른 선택은 TypeScript나 JavaScript를 배우고 Angular나 React, Vue.js 중 하나를 학습하는 것이지만 JavaScript를 master 하는 데 걸리는 시간과 들여야 하는 노력이 필요합니다.
3. Blazor 시작하기
Blazor를 시작하기 위해 이전 글에서 완성한 예제 application에서 Blazor를 사용하도록 하고 controller와 Razor Page에서 제공된 기능적인 부분을 재작성해볼 것입니다.
그런 후 다시 기본으로 되돌아가 Razor component가 작동하는 방식과 여기서 제공하는 기능의 차이를 알아보고자 합니다.
(1) Blazor Server를 위한 ASP.NET Core 구성
Blazor를 사용하기 이전에 몇가지 준비사항이 필요합니다. 우선 아래와 같이 project의 Program.cs에서 필요한 service와 middleware를 추가해야 합니다.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddDbContext<NorthwindContext>(opts => {
opts.UseSqlServer(builder.Configuration["ConnectionStrings:NorthwindConnection"]);
opts.EnableSensitiveDataLogging(true);
});
var app = builder.Build();
//app.MapGet("/", () => "Hello World!");
app.UseStaticFiles();
app.MapControllers();
app.MapControllerRoute("controllers", "controllers/{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.MapBlazorHub();
MapBlazorHub method에서 'hub'는 persistent HTTP요청을 처리하는 ASP.NET Core의 일부인 signalR과 관련이 있습니다. SignalR은 그렇게 흔하게 사용되는 방식은 아니지만 client와 server사이에 지속적인 연결이 필요한 경우 유용한 해결책이 될 수 있습니다. 자세한 사항은 아래 글을 참고하시기 바라며
Overview of ASP.NET Core SignalR | Microsoft Learn
지금은 그냥 signalR이 Blazor에 의존하는 연결을 관리하기 위해 사용된다는 점만 알고 있으면 충분합니다.
● Layout에 Blazor JavaScript File 추가하기
Blazor는 ASP.NET Core Server와의 통신을 위해 JavaScript code에 의존합니다. 따라서 아래와 같이 controller view에서 사용되는 Views/Shared의 _Layout.cshtml file에서 layout으로 JavaScript file을 추가합니다.
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<base href="~/" />
</head>
<body>
<div class="m-2">
@RenderBody()
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
script요소에서는 javascript file명을 지정하고 있는데 해당 file의 요청은 위 예제에서 요청 pipeline으로 추가된 middleware에서 가로채어 처리되는데 이때 JavaScript code를 project에 추가하기 위해 필요한 별도의 package 같은 건 없습니다. 또한 base요소는 application에서 최상위 URL을 지정하기 위해 추가되어야 합니다. 위와 동일한 요소는 Pages folder의 _Layout.cshtml에서와 같이 Razor Page에서 사용되는 layout에서도 동일하게 추가되어야 합니다.
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<base href="~/" />
</head>
<body>
<div class="m-2">
<h5 class="bg-secondary text-white text-center p-2">Razor Page</h5>
@RenderBody()
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
● Blazor Imports File 생성
Blazor는 사용할 namespace를 지정하기 위해서는 자체 import file이 필요합니다. project에 해당 file를 추가하는 것은 잊어버리기 쉽지만 실제 file을 import해주지 않으면 Blazor는 작동하지 않을 것입니다. project에 _Imports.razor file을 아래와 같이 추가합니다.(Visual Studio에서는 file을 생성하기 위해 Razor View imports template을 사용할 수 있지만 file의 확장자는 반드시 .razor여야 합니다.)
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Microsoft.EntityFrameworkCore
@using MyBlazorApp.Models
예제에서 처음 5개 @using표현식은 Blazor에서 필요한 namespace에 해당합니다. 그리고 나머지 2개 표현식은 예제에서의 편의를 위한 것인데 Entity Framework Core와 Models namespace내부의 class를 기본적으로 사용할 수 있도록 하는 것입니다.
(2) Razor Component 생성하기
우선 용어에 대한 오해가 있을 수 있는데 기술 자체는 Blazor이지만 핵심 구성 요소는 Razor Component라고 합니다. Razor Component는 .razor라는 확장자를 가진 file을 통해 정의되며 file명 자체는 대문자로 시작되어야 합니다. Component는 어느 위치에든 정의될 수 있으나 한 곳에서 모아지는 형태로 정의되어 project구조가 잘 정리되도록 하는 것이 일반적입니다. project의 Advenced folder안에 Blazor folder를 생성하고 ProductList.razor이름의 Razor component를 아래와 같이 추가합니다.
<table class="table table-sm table-bordered table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name (Price)</th>
<th>Category</th>
<th>Manufacturer</th>
</tr>
</thead>
<tbody>
@foreach (Product p in Product ?? Enumerable.Empty<Product>())
{
<tr class="@GetClass(p.ProductManufacturer?.ManufacturerName)">
<td>@p.ProductId</td>
<td>@p.ProductName, @p.ProductPrice?.ToString("#,##0")</td>
<td>@p.ProductCategory.CategoryName</td>
<td>@p.ProductManufacturer?.ManufacturerName</td>
</tr>
}
</tbody>
</table>
<form asp-action="Index" method="get">
<div class="form-group">
<label for="selectedManufacturer">Manufacturer</label>
<select name="selectedManufacturer" class="form-control" @bind="SelectedManufacturer">
<option disabled selected>Select Manufacturer</option>
@foreach (string Manufacturer in Manufacturer ?? Enumerable.Empty<string>())
{
<option value="@Manufacturer" selected="@(Manufacturer == SelectedManufacturer)">
@Manufacturer
</option>
}
</select>
</div>
<button class="btn btn-primary mt-2" type="submit">Select</button>
</form>
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Product>? Product => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer);
public IEnumerable<string>? Manufacturer => Context?.Manufacturer.Select(m => m.ManufacturerName);
public string SelectedManufacturer { get; set; } = string.Empty;
public string GetClass(string? Manufacturer) => SelectedManufacturer == Manufacturer ? "bg-info text-white" : "";
}
Razor Component는 Razor Page와 비슷한 구조를 가지고 있는데 view영역은 component의 HTML안으로 data값을 넣거나 다음과 같이 배열에서 개체를 생성하기 위한 @표현식으로 Razor 기능에 의존한다는 것을 알 수 있습니다.
@foreach (string Manufacturer in Model?.Manufacturer ?? Enumerable.Empty<string>())
{
<option selected="@(Manufacturer == Model?.SelectedManufacturer)">
@Manufacturer
</option>
}
예제에서 @foreach 표현식은 Manufacturer 배열에 대한 각 값의 option 요소를 생성하고 있는데 지금까지 봐왔던 controller나 Razor Page에서 생성했던 것들과 다르지 않습니다.
비록 Razor component가 비슷해 보이기는 하지만 몇몇 중요한 차이가 존재합니다. 첫번째로 page model class가 없으며 @model 표현식이 없습니다. component의 HTML을 지원하는 속성과 method는 @code 표현식 안에서 직접 정의되며 이것은 곧 Razor Page @functions 표현식에 대응됩니다. 예를 들어 view영역에 Person 개체를 제공할 속성을 정의하기 위해서는 @code 영역 안에서 아래와 같이 People 속성을 정의하면 됩니다.
public IEnumerable<Product>? People => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer);
또한 page model class도 존재하지 않기에 이를 통해 service의존성을 정의할 생성자 역시도 존재하지 않습니다. 대신 의존성 주입은 아래와 같이 Inject attribute가 적용된 속성의 값을 설정합니다.
[Inject]
public BlazorTDBContext? Context { get; set; }
가장 중요한 차이점은 select 요소에서 특정한 attribute를 사용한다는 것입니다.
<select name="selectedManufacturer" class="form-control" @bind="SelectedManufacturer">
예제의 Blazor attribute는 select요소의 값과 @code영역안에 정의된 SelectedManufacturer속성사이에 data binding을 생성합니다.
추후에 data binding에대해 자세히 설명하겠지만 지금은 SelectedManufacturer의 값은 사용자가 select요소의 값을 변경할 때 같이 바뀐다는 점만 알고 있으면 충분합니다.
● Razor component의 사용
Razor component는 Razor Page 또는 controller view의 일부로 browser에 제공됩니다. 아래 예제에서는 Views/Home folder에 있는 Index.cshtml file의 controller view에서 Razor Component를 어떻게 사용할 수 있는지를 나타내고 있습니다.
<h4 class="bg-primary text-white text-center p-2">Product</h4>
<component type="typeof(MyBlazorApp.Advanced.Blazor.ProductList)" render-mode="Server" />
Razor component는 tag helper중 하나인 component요소를 사용해 적용되며 type과 render-mode attribute를 사용해 설정됩니다. 여기서 type attribute는 Razor Component자체를 지정하기 위한 것이고 controller view나 Razor page와 같이 class로 compile 됩니다. ProductList component는 해당 project의 Blazor folder에서 정의되었으므로 type에서는 MyBlazorApp.Blazor.ProductList가 지정되는 것입니다.
render-mode attribute는 component에서 content를 어떤 방식으로 생성할지를 지정합니다. 여기서 해당 attribute의 값은 아래 표에서와 같이 RenderMode열거형을 사용할 수 있습니다.
Static | Razor component는 view section을 client-side를 지원하지 않는 정적 HTML로서 render합니다. |
Server | HTML 문서는 component의 placeholder와 함께 browser로 전송되며 이렇게 component에서 표시된 HTML은 지속적 HTTP연결을 통해 browser로 전송되고 사용자에게 표시됩니다. |
ServerPrerendered | component의 view영역은 HTML에 포함되어 사용자에게 즉시 표시됩니다. HTML content는 지속적 HTTP연결을 통해 다시 전송됩니다. |
대부분의 application에서 Server option은 좋은 선택이 될 수 있습니다. ServerPrerendered는 browser로 전송되는 HTML문서에서 Razor component의 view 영역을 정적으로 render하는 기능이 포함되어 있습니다. 이것은 placeholder content와 같은 동작으로 JavaScript code가 load 되고 실행되는 동안 사용자에게 빈 browser화면이 표시되지 않도록 합니다. 그런 후 일단 지속적 HTTP연결이 성립되면 placeholder content는 삭제되고 Blazor에 의해 전송된 동적 version의 content로 바뀌게 됩니다. 물론 사용자에게 정적 content를 보여주는 것은 좋은 생각이긴 하지만 HTML요소가 application의 server-side부분 과 연결되어 있지 않고 따라서 사용자와의 모든 상호작용이 동작하지 않거나, 실제 content가 도착하면 폐기됨으로써 나름대로의 부자연적인 동작을 수행할 수도 있습니다.
action에서의 Blazor를 확인해 보기 위해 project를 실행하고 /controllers URL을 요청합니다. Blazor를 사용하면 form을 전송하는것은 필요하지 않습니다. 왜냐하면 data binding은 select요소의 값이 바뀌자마자 다음과 같이 응답되기 때문입니다.
사용자가 select요소를 선택하게 되면 선택한 값은 지속적 HTTP 연결을 통해 ASP.NET Core Server로 전송되고 이어서 Razor component의 SelectedManufacturer속성이 update되며 HTML content가 다시 render 됩니다. 일련의 update는 JavaScript code로 전송되어 table이 update 되는 것입니다.
Razor Component는 또한 Razor Page에서도 사용될 수 있습니다. Pages folder에 Blazor.cshtml이름의 Razor Page를 아래와 같이 추가합니다.
@page "/pages/blazor"
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("markElems").addEventListener("click", () => {
document.querySelectorAll("td:first-child").forEach(elem => {
elem.innerText = `M:${elem.innerText}`
elem.classList.add("border", "border-dark");
});
});
});
</script>
<h4 class="bg-primary text-white text-center p-2">Blazor Product</h4>
<button id="markElems" class="btn btn-outline-primary mb-2">Mark Elements</button>
<component type="typeof(MyBlazorApp.Blazor.ProductList)" render-mode="Server" />
예제의 Razor Page에서는 새로운 HTML table전체대신 변경사항만 전송된다는 것을 보여주는데 도움이 되는 추가적인 JavaScript를 포함하고 있습니다. Project를 실행하고 /pages/blazor로 URL을 요청합니다. Mark Elements button을 click하면 ID Column의 Cell이 변경되어 다른 content와 border가 표시될 것입니다. 그 상태에서 select요소를 사용하여 다른 Manufacturer를 선택하게 되면 table의 요소가 삭제되지 않고 그대로 수정되는 것을 알 수 있습니다.
Blazor 연결 message
Visual Studio가 아닌 console을 통해 ASP.NET Core를 실행시킨 경우 그대로 동작을 멈추게 되면 browser에서 error를 보게 될 수도 있습니다. 이 것은 server와 연결이 끊어졌음을 의미하는 것으로 따라서 사용자는 현재 표시된 component와 계속 상호작용하는 것을 방지할 수 있습니다. Blazor는 재연결을 시도하며 일시적인 network문제로 인해 비연결상황이 발생한 경우 중단된 위치를 선택하게 됩니다. 하지만 server가 멈추거나 재시작되면 이러한 동작은 연결을 위한 context data가 손실되므로 동작이 불가능하게 되고 따라서 명시적으로 URL을 새롭게 다시 요청해야 합니다.
connection message에는 기본 새로고침 link가 존재하기는 하지만 예제에서는 예제의 동작을 확인해 보기 위한 URL을 직접 지정하고 있으므로, 현재상태에서 website의 기본 URL로 이동하는 것은 그다지 유용한 해결책은 아닙니다.
4. 기본 Razor Component 기능
위에서는 Blazor가 어떻게 사용되고 어떻게 작동하는지를 살펴봤습니다. 이제 다시 기본으로 되돌아가 Razor Component가 제공하는 기능들에 대해 알아보고자 합니다. 비록 이전 예제는 기본적인 ASP.NET Core기능이 어떻게 Blazor를 사용해 재현될 수 있는지를 봤었지만 훨씬 더 유용한 일련의 기능들이 존재합니다.
(1) Blazor Event와 Data Binding
Event는 Razor Component가 사용자와의 상호작용에 응답하는 것이며 이를 위해 Blazor는 event의 상세를 처리가능한 server에 전송하기 위해 지속적인 HTTP 연결을 사용합니다. action에서의 Blazor event를 확인해 보기 위해 Events.razor이름이 Razor Component를 Blazor folder에 아래와 같이 추가합니다.
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="IncrementCounter">Increment</button>
<span class="p-2">Counter Value: @Counter</span>
</div>
@code {
public int Counter { get; set; } = 1;
public void IncrementCounter(MouseEventArgs e) {
Counter++;
}
}
event의 handler(처리자)는 HTML요소에 attribute를 추가함으로서 등록할 수 있습니다. 예제에서 attribute의 이름은 @on으로 시작하며 그다음에 event의 이름이 따라옵니다. 예제에서는 button요소에서 생성된 click event에 대한 처리자를 설정하는 것이므로 event는 @onclick이 됩니다.
attribute에 할당된 값은 event가 trigger될때 호출되는 method의 이름입니다. method는 EventArgs class의 instance 혹은 event에 대한 추가적인 정보를 제공하는 EventArgs로부터 파생된 class와 같이 선택적인 매개변수를 정의할 수 있습니다.
onclick evnet를 위해 handler method는 click시 화면좌표와 같은 추가적인 상세를 제공하는 MouseEventArgs개체를 전달받는데 아래 표에서는 사용가능한 EventArgs class와 해당 class가 표현하는 event를 나타내고 있습니다.
ChangeEventArgs | onchange, oninput |
ClipboardEventArgs | oncopy, oncut, onpaste |
DragEventArgs | ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop |
ErrorEventArgs | onerror |
FocusEventArgs | onblur, onfocus, onfocusin, onfocusout |
KeyboardEventArgs | onkeydown, onkeypress, onkeyup |
MouseEventArgs | onclick, oncontextmenu, ondblclick, onmousedown, onmousemove, onmouseout, onmouseover, onmouseup, onmousewheel, onwheel |
PointerEventArgs | ongotpointercapture, onlostpointercapture, onpointercancel, onpointerdown, onpointerenter, onpointerleave, onpointermove, onpointerout, onpointerover, onpointerup |
ProgressEventArgs | onabort, onload, onloadend, onloadstart, onprogress, ontimeout |
TouchEventArgs | ontouchcancel, ontouchend, ontouchenter, ontouchleave, ontouchmove, ontouchstart |
EventArgs | onactivate, onbeforeactivate, onbeforecopy, onbeforecut, onbeforedeactivate, onbeforepaste, oncanplay, oncanplaythrough, oncuechange, ondeactivate, ondurationchange, onemptied, onended, onfullscreenchange, onfullscreenerror, oninvalid, onloadeddata, onloadedmetadata, onpause, onplay, onplaying, onpointerlockchange, onpointerlockerror, onratechange, onreadystatechange, onreset, onscroll, onseeked, onseeking, onselect, onselectionchange, onselectstart, onstalled, onstop, onsubmit, onsuspend, ontimeupdate, onvolumechange, onwaiting |
Blazor JavaScript code는 trigger되는 event를 수신하고 이것을 지속적인 HTTP 연결을 통해 server로 전송합니다. 이에 handler method가 호출되고 비로소 component의 상태가 update 되는 것입니다. component의 view영역에서 생성된 content의 모든 변경사항은 JavaScript code로 되돌려지고 browser에 표시된 content를 update 하게 됩니다.
예제에서 click event는 IncrementCounter method에서 처리되고 곧 Counter 속성의 값을 변경합니다. Counter 속성의 값은 component에 의해 render된 HTML에 포함되어 있으므로 Blazor browser로 변경사항을 전송하고 JavaScript code는 사용자에게 표시된 HTML요소를 update 합니다. 이제 Events component를 표시하기 위해 Pages folder에 있는 Blazor.cshtml file의 content를 아래와 같이 새로운 component로 교체합니다.
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Events</h4>
<component type="typeof(MyBlazorApp.Blazor.Events)" render-mode="Server" />
위 예제에서는 component요소의 type attribute를 변경하고 기존의 JavaScript를 제거하였으며 이전 예제에서 요소를 표시하기 위해 사용한 button요소 또한 제거하였습니다. project를 실행하고 /pages/blazor로 URL을 요청하여 새로운 component가 응답되는지를 확인합니다. 그런 뒤 Increment button click 하면 click event는 Blazor JavaScript code에 의해 수신되고 IncrementCounter method에서의 처리를 위해 server로 전송하여 아래와 같은 응답을 생성하게 됩니다
● 다수 요소로 부터의 event 처리
code의 중복을 피하기 위해 다수 요소에서 요소들은 Blazor folder의 Evnets.razor file에서와 같이 단일 handler method를 수신할 수 있습니다.
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="@(e => IncrementCounter(e, 0))">
Increment Counter #1
</button>
<span class="p-2">Counter Value: @Counter[0]</span>
</div>
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="@(e => IncrementCounter(e, 1))">
Increment Counter #2
</button>
<span class="p-2">Counter Value: @Counter[1]</span>
</div>
@code {
public int[] Counter { get; set; } = new int[] { 1, 1 };
public void IncrementCounter(MouseEventArgs e, int index)
{
Counter[index]++;
}
}
Blazor event attribute는 EventArge개체를 수신하는 lambda함수를 통해 사용될 수 있으며 추가적인 매개변수와 함께 handler method를 호출할 수 있습니다. 위 예제에서는 IncrementCounter method에 update될 counter값을 결정할 때 사용될 index 매개변수를 추가했으며 매개변수의 값은 @onclick attribute에서 정의됩니다.
이러한 기법은 요소를 동적으로 생성할때도 아래와 같이 사용될 수 있습니다.
@for (int i = 0; i < ElementCount; i++)
{
int local = i;
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(local))">
Increment Counter #@(i + 1)
</button>
<span class="p-2">Counter Value: @GetCounter(i)</span>
</div>
}
@code {
public int ElementCount { get; set; } = 4;
public Dictionary<int, int> Counters { get; } = new Dictionary<int, int>();
public int GetCounter(int index) => Counters.ContainsKey(index) ? Counters[index] : 0;
public void IncrementCounter(int index) => Counters[index] = GetCounter(index) + 1;
}
예제에서는 요소를 생성하기 위해 @for 표현식을 사용했으며 handle method의 매개변수로서 loop변수를 사용했습니다. 또한 handler method에서 더 이상 사용되지 않는 EventArgs 매개변수를 제거하였습니다.
handler method 이름을 지정할때 주의사항
event handler method를 특정할때 가장 일반적으로 하는 실수는 다음과 같이 괄호를 포함하는 것입니다.
<button class="btn btn-primary" @onclick="IncrementCounter()">
여기서 생성되는 오류 message는 event handler method에 따라 달라지는데 아마도 형식 매개변수가 없거나 void를 EventCallback으로 변환할 수 없다는 경고를 보게 될 것입니다. 따라서 handler method를 지정할 때는 정확히 event명만을 지정해야 합니다.
<button class="btn btn-primary" @onclick="IncrementCounter">
혹은 아래와 같이 Razor 표현식을 사용할 수도 있습니다.
<button class="btn btn-primary" @onclick="@IncrementCounter">
일부 개발자들은 이러한 방법이 읽기에는 더 쉽다고 생각하기도 하지만 결과적으로는 같은것입니다. Razor 표현식 안에서 정의되어야 하는 lambda함수를 사용하면 다른 설정규칙이 아래와 같이 적용됩니다.
<button class="btn btn-primary" @onclick="@( ... )">
Razor표현식 안에서 lambda함수는 C# class안에서 처럼 정의될 수 있으며 이는 화살표와 함수본체를 사용함으로써 매개변수를 정의할 수 있다는 의미가 됩니다.
<button class="btn btn-primary" @onclick="@((e) => HandleEvent(e, local))">
만약 EventArgs개체가 필요하지 않다면 lambda함수에서 매개변수는 생략할 수 있습니다.
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(local))">
비록 처음에는 산만하게 보일 수 있으나 Blazor를 계속해서 사용하면 이러한 규칙에 자연스럽게 익숙해질 것입니다.
evnet handler를 이해하는데 중요한 점은 @onclick lambda함수는 server가 browser로 부터 event를 수신할 때까지 실행되지 않는다는 것입니다. 따라서 loop변수 i를 IncrementCounter method의 매개변수로 사용하지 않도록 주의해야 합니다. 왜냐하면 항상 loop에 의해 생성된 마지막값이 사용될 것이기 때문입니다.(예제의 경우 4) 대신 지역변수를 통해 loop변수를 잡아내야 합니다.
int local = i;
그런 다음 해당 지역변수를 attribute에서 event handler method에 대한 매개변수로서 사용해야 합니다.
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(local))">
지역변수는 각각 생성된 요소의 lambda함숫값을 고정합니다. project를 실행하고 /pages/blazor로 URL을 요청하면 아래와 같은 응답이 생성되는데
모든 button 요소에서 생성된 click event는 같은 method에서 처리될 것이지만 현재 counter값을 가진 lambda함수에서 제공된 매개변수는 각각 개별적으로 update 됨을 알 수 있습니다.
● handle method없이 Event 처리하기
간단한 event처리의 경우에는 lambda함수를 통해 handler method를 사용하지 않고 아래 Blazor folder의 Events.razor file에서와 같이 직접 처리할 수 있습니다.
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(local))">
Increment Counter #@(i + 1)
</button>
<button class="btn btn-info" @onclick="@(() => Counters.Remove(local))">
Reset
</button>
<span class="p-2">Counter Value: @GetCounter(i)</span>
복잡한 handler의 경우 method로서 정의될 수 있지만 위 예제에서와 같이 비교적 단순한 handler의 경우는 code의 양을 간소화시킬 수 있습니다. project를 실행하고 /pages/blazor URL을 호출한 뒤 Reset button을 누르면 component의 code영역에서 method를 따로 정의하지 않고도 Conuters collection으로부터 값을 삭제할 수 있음을 알 수 있습니다.
● 기본 Event와 Event 전파 차단하기
Blazor는 2개의 attribute를 제공함으로서 browser event의 기본 동작을 아래 표에서 설명된 것과 같이 변경할 수 있습니다. event의 이름이 오고 그다음 colon에 뒤이어 keyword가 오는 이들 attribute는 매개변수로도 알려져 있습니다.
@on{event}:preventDefault | 이 매개변수는 요소의 기본 event가 trigger되었는지의 여부를 확인합니다. |
@on{event}:stopPropagation | 이 매개변수는 event가 부모요소로 전파되었는지 여부를 확인합니다. |
아래 예제는 Blazor folder의 Events.razor file에서 event기본값을 재정의한 것으로 이들 매개변수가 무엇을 하는지, 이들이 왜 유용한지를 예제를 통해 알 수 있습니다.
<form action="/pages/blazor" method="get">
@for (int i = 0; i < ElementCount; i++)
{
int local = i;
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(local))" @onclick:preventDefault="EnableEventParams">
Increment Counter #@(i + 1)
</button>
<button class="btn btn-info" @onclick="@(() => Counters.Remove(local))">
Reset
</button>
<span class="p-2">Counter Value: @GetCounter(i)</span>
</div>
}
</form>
<div class="m-2" @onclick="@(() => IncrementCounter(1))">
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(0))" @onclick:stopPropagation="EnableEventParams">
Propagation Test
</button>
</div>
<div class="form-check m-2">
<input class="form-check-input" type="checkbox" @onchange="@(() => EnableEventParams = !EnableEventParams)" />
<label class="form-check-label">Enable Event Parameters</label>
</div>
@code {
public int ElementCount { get; set; } = 4;
public Dictionary<int, int> Counters { get; } = new Dictionary<int, int>();
public int GetCounter(int index) => Counters.ContainsKey(index) ? Counters[index] : 0;
public void IncrementCounter(int index) => Counters[index] = GetCounter(index) + 1;
public bool EnableEventParams { get; set; } = false;
}
위 예제는 browser의 event에 대한 기본동작이 발생시킬 수 있는 문제점에 대한 2가지 상황을 만들고 있습니다. 그 중 첫 번째는 form요소를 추가함으로 인해 발생되는 것입니다. 기본적으로 form에 포함되는 기본 button요소는 @onclick attribute가 존재하는 상황에서도 click이 되면 form을 submit 할 수 있습니다. 이로 인해 Increment Counter button이 click이 되면 언제든지 browser는 Blazor.cshtml Razor Page의 content를 응답할 ASP.NET Core server로 form을 전송하게 됩니다.
두 번째 문제는 event handler를 정의하고 있는 부모요소에 의한 것입니다.
<div class="m-2" @onclick="@(() => IncrementCounter(1))">
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(0))" @onclick:stopPropagation="EnableEventParams">
Propagation Test
</button>
</div>
Event는 browser안에서 잘 정의된 생명주기를 거치게 되는데 여기에는 부모 요소의 chain위로 전달되는 것을 포함합니다. 따라서 예제에서 button을 click 하는 것은 button요소의 @onclick handler에 의한 것과 div요소에서의 @onclick handler에 의한 것 이렇게 2개의 counter가 update 됨을 의미하게 됩니다.
project를 실행하여 /pages/blazor로 URL을 요청하고 Increment Counter button을 click하면 form이 submit 되는데 이때 page가 다시 load 될 것입니다. 또한 Propagation Test button을 click 하면 2개의 counter가 update 됨을 확인할 수 있습니다.
또한 예제에서의 chckbox는 위 table에서 설명된 매개변수를 적용하는 속성을 toggle하며 이것으로 form이 submit 되지 않고 button의 handler만 event를 수신할 수 있는 효과를 가져오게 됩니다. checkbox를 check 하고 Inrement Counter button과 Propagation Test button을 click 하여 다음과 같이 동작하는 효과를 확인해 봅니다.
(2) Data Binding 하기
Event handler와 Razor 표현식은 HTML요소와 C#값간에 상호관계를 생성하는 것에도 사용될 수 있으며 이것은 input이나 select요소와 같이 사용자가 값을 변경할 수 있는 요소에서 즉각적인 반응을 유도하는 데 사용될 수 있습니다. Blazor folder에 Bindings.razor이름의 Razor Component file을 아래와 추가합니다.
<div class="form-group">
<label>Manufacturer:</label>
<input class="form-control" value="@Manufacturer" @onchange="UpdateManufacturer" />
</div>
<div class="p-2 mb-2">Manufacturer Value: @Manufacturer</div>
<button class="btn btn-primary" @onclick="@(() => Manufacturer = "SAM")">SAM</button>
<button class="btn btn-primary" @onclick="@(() => Manufacturer = "HY")">HY</button>
@code {
public string? Manufacturer { get; set; } = "INT";
public void UpdateManufacturer(ChangeEventArgs e)
{
Manufacturer = e.Value as string;
}
}
예제에서 @onchange attribute는 UpdateManafacturer method를 input요소에서 change event에 대한 handler로서 등록하고 있습니다. 여기서 event는 Value속성을 제공하는 ChangeEventArgs class로 나타낼 수 있습니다. Manufacturer속성은 change event가 수신될때마다 input요소의 content로 update 됩니다.
input요소의 value attribute는 다른 방향으로 관계를 형성하므로 Manufacturer속성의 값의 바뀔 때 요소의 값도 바뀌면서 사용자에게 표시된 text도 같이 바뀌게 됩니다. 위 예제의 Razor Component를 적용하기 위해 Pages folder의 Blazor.cshtml file에서 Razor Page component attribute를 아래와 같이 변경합니다.
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Events</h4>
<component type="typeof(MyBlazorApp.Blazor.Bindings)" render-mode="Server" />
예제에서 정의된 관계의 두 부분을 모두 확인해 보기 위해 project를 실행하고 /pages/blazor URL을 요청한 후 input요소의 값을 변경합니다. change event는 input요소에서 focus가 벗어나면 trigger 되므로 input요소의 변경이 끝나면 Tab key나 input요소의 외부를 click 합니다. 그러면 입력된 값이 div요소의 Razor 표현식을 통해 다음과 같이 표시될 것입니다.
또한 아래 button을 click 하면 Manufacturer속성은 SAM 또는 HY로 바뀌게 될 것이며 선택된 값은 div요소와 input요소 모두에 다음과 같이 표현될 것입니다.
event의 변경과 관련된 상호관계는 Blazor folder의 Bindings.razor file에서 Data Binding을 사용한 것처럼 값과 event모두 단일 attribute를 통해 설정될 수 있는 data binding을 표현하는 데에도 사용될 수 있습니다.
<div class="form-group">
<label>Manufacturer:</label>
<input class="form-control" @bind="Manufacturer" />
</div>
<div class="p-2 mb-2">Manufacturer Value: @Manufacturer</div>
<button class="btn btn-primary" @onclick="@(() => Manufacturer = "SAM")">SAM</button>
<button class="btn btn-primary" @onclick="@(() => Manufacturer = "HY")">HY</button>
@code {
public string? Manufacturer { get; set; } = "INT";
//public void UpdateManufacturer(ChangeEventArgs e)
//{
// Manufacturer = e.Value as string;
//}
}
@bind attribute는 change event가 trigger 되면 update 될 속성을 지정하는 데 사용되며 실제 값이 바뀌게 되면 value attribute가 update 됩니다. 이 것은 이전예제와 같은 동작을 하면서도 더욱 간소화된 code로 표현되었으며 속성을 update 하기 위한 handler method 혹은 lambda함수를 필요로 하지 않습니다.
● Binding Event 변경
기본적으로 변경 event는 server로부터 너무 많은 update가 필요하지 않으면서 사용자에게 효율적인 반응성을 제공하는 binding에 사용됩니다. binding에 사용되는 event는 아래 표에 나열된 attribute를 사용해 변경할 수 있습니다.
@bind-value | 이 attribute는 data binding의 속성을 선택하는데 사용됩니다. |
@bind-value:event | 이 attribute는 data binding의 event를 선택하는데 사용됩니다. |
이들 attribute는 Blazor folder의 Bindings.razor file에서 Binding을 위한 Event를 지정한 것처럼 @bind를 대신해 사용되지만 ChangeEventArgs class로 표현되는 event를 통해서만 사용될 수 있습니다. 다시 말해 적어도 현재 release에서는 onchange와 oninput event에서만 사용될 수 있습니다.
<div class="form-group">
<label>Manufacturer:</label>
<input class="form-control" @bind-value="Manufacturer" @bind-value:event="oninput" />
</div>
예제에서의 attribute결합은 oninput event(input요소가 focus를 잃을 때보다는 모든 key 눌림후에 발생되는)가 trigger될때 update되는 Manufacturer속성을 위한 binding을 생성합니다. project를 실행하고 /pages/blazor로 URL을 요청한 뒤 input요소에 입력을 시도합니다. 그러면 Manufacturer속성이 key눌림 후 update 될 것입니다.
● DateTime Binding 생성
Blazor는 특별히 DateTime 속성에 대한 binding을 생성을 지원하고 있으며 이를 통해 특정한 문화권 또는 형식문자열을 사용해 DateTime을 표현할 수 있습니다. 해당 기능은 아래 표에 설명된 매개변수를 사용해 적용됩니다.
@bind-value과 @bind-value:event attribute를 event를 지정하기 위해 사용했다면 @bind-value:culture와 @bind-value:format 매개변수를 대신 사용해야 합니다.
@bind:culture | 해당 attribute는 DateTime값에 대한 형식에 사용될 CultureInfo개체를 선택하는데 사용됩니다. |
@bind:format | 해당 attribute는 DateTime값에 대한 형식에 사용될 data 형식 문자열을 지정하는데 사용됩니다. |
아래 예제에서는 Blazor folder의 Bindings.razor file에서 DateTime속성을 사용한 것으로 해당 속성을 통해 위 attribute를 어떻게 사용할 수 있는지를 나타내고 있습니다.
@using System.Globalization
<div class="form-group">
<label>Manufacturer:</label>
<input class="form-control" @bind-value="Manufacturer" @bind-value:event="oninput" />
</div>
<div class="p-2 mb-2">Manufacturer Value: @Manufacturer</div>
<button class="btn btn-primary" @onclick="@(() => Manufacturer = "SAM")">SAM</button>
<button class="btn btn-primary" @onclick="@(() => Manufacturer = "HY")">HY</button>
<div class="form-group mt-2">
<label>Time:</label>
<input class="form-control my-1" @bind="Time" @bind:culture="Culture"
@bind:format="MMM-dd" />
<input class="form-control my-1" @bind="Time" @bind:culture="Culture" />
<input class="form-control" type="date" @bind="Time" />
</div>
<div class="p-2 mb-2">Time Value: @Time</div>
<div class="form-group">
<label>Culture:</label>
<select class="form-control" @bind="Culture">
<option value="@CultureInfo.GetCultureInfo("ko-kr")">ko-KR</option>
<option value="@CultureInfo.GetCultureInfo("en-us")">en-US</option>
<option value="@CultureInfo.GetCultureInfo("en-gb")">en-GB</option>
</select>
</div>
@code {
public string? Manufacturer { get; set; } = "INT";
public DateTime Time { get; set; } = DateTime.Parse("2050/01/20 09:50");
public CultureInfo Culture { get; set; } = CultureInfo.GetCultureInfo("en-us");
}
예제에서는 같은 DateTime값을 표시하기 위해 사용되는 3개의 input요소가 존재하며 이들 중 2개는 상기 Table에 명시된 attribute를 사용해 설정되었고 다시 첫 번째 요소는 culture와 format이 모두 사용되었습니다.
DateTime속성은 select요소의 선택된 culture와 요약된 월의 이름과 일수를 표시하도록 하는 형식문자열을 사용해 표시되었습니다. 두 번째 입력 요소는 culture만을 지정하는데 이는 기본 형식 문자열이 사용될 것임을 의미합니다.
이렇게 해서 날짜가 어떻게 표시될지를 확인하기 위해 project를 실행하고 /pages/blazor URL을 요청합니다. 그리고 select요소를 통해 다른 문화권설정을 선택합니다. 해당 설정에서는 대한민국에서 사용하는 한국어, 미국에서 사용되는 영어권, 그리고 영국에서 사용하는 영어권을 선택할 수 있습니다.
예제에서의 초기 locale은 en-US이며 이 상태에서 다른 문화권으로 설정을 변경하면 date와 time의 형식이 같이 변경될 것입니다.
Browser의 date형식 허용하기
예제에서 3번째 input요소에 표시된 값은 선택한 문화권과는 관계없이 바뀌지 않습니다. 해당 input요소는 상기 표에 명시된 attribute를 가지고 있지 않지만 date를 설정하는 type attribute는 가지고 있습니다. type을 date나 datetime-local, month 또는 time로 설정하는 경우에는 특정 문화권이나 형식문자열을 지정할 수 없습니다. 왜냐하면 Blazor는 자동으로 형식 date값을 browser가 사용자의 locale로 변환하는 culture-neutral형식을 사용하기 때문입니다. 위 응답화면에서 date는 en-US locale로 형식화된 결과를 보여주고 있지만 실제 사용자는 자신들의 지역화에 맞게 변환된 date를 보게 될 것입니다.
5. Component를 정의하기 위한 Class File사용
C# code와 Razor Component를 지원하는 markup을 섞어서 쓰는 걸 좋아하지 않는다면 C# class file을 component의 일부분 혹은 전체로 정의하여 사용할 수 있습니다.
(1) Code-Behind Class 사용
Razor component의 @code영역은 code-behind class 또는 code-behind file로 알려진 class file로 분리하여 정의될 수 있습니다. Razor component에 대한 Code-behind class는 code를 제공하는 component와 같은 이름을 통해 partial class로 정의됩니다.
Blazor folder에 Split.razor이라는 이름의 Razor Component를 아래와 같이 추가합니다.
<ul class="list-group">
@foreach (string name in Names)
{
<li class="list-group-item">@name</li>
}
</ul>
해당 file은 오로지 HTML content와 Razor 표현식만을 포함하고 있으며 Names속성을 통해 수신될 name의 목록을 render 하고 있습니다. 해당 component에 대한 code를 제공하기 위해 이번에는 Split.razor.cs라는 이름의 file을 같은 folder에 아래와 같이 추가하되 class는 partial class로 정의합니다.
using Microsoft.AspNetCore.Components;
using MyBlazorApp.Models;
namespace MyBlazorApp.Blazor
{
public partial class Split
{
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<string> Names => Context?.Product.Select(p => p.ProductName) ?? Enumerable.Empty<string>();
}
}
partial class는 반드시 Razor Component와 같은 namespace로 정의되어야 하며 같은 이름을 가져야 합니다. 예제에서 namespace는 MyBlazorApp.Blazor이고 class의 이름은 Split입니다. 또한 Code-behind class는 생성자가 아닌 Inject attribute를 사용해 service를 전달받아야 합니다.
이제 Pages folder에 있는 Blazor.cshtml file에서 새로운 Component를 다음과 같이 적용합니다.
e "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Code-Behind</h4>
<component type="typeof(MyBlazorApp.Blazor.Split)" render-mode="Server" />
project를 실행하고 /pages/blazor URL을 요청하면 아래와 같은 응답을 볼 수 있습니다.
(2) Razor Component Class정의
Razor Component는 비록 Razor표현식보다 표현력이 떨어지기는 하지만 class file에서 전체적으로 정의할 수 있습니다. Blazor folder안에 CodeOnly.cs라는 이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using MyBlazorApp.Models;
namespace MyBlazorApp.Blazor
{
public class CodeOnly : ComponentBase
{
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<string> Names => Context?.Product.Select(p => p.ProductName) ?? Enumerable.Empty<string>();
public bool Ascending { get; set; } = false;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
IEnumerable<string> data = Ascending ? Names.OrderBy(n => n) : Names.OrderByDescending(n => n);
builder.OpenElement(1, "button");
builder.AddAttribute(2, "class", "btn btn-primary mb-2");
builder.AddAttribute(3, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, () => Ascending = !Ascending));
builder.AddContent(4, new MarkupString("Toggle"));
builder.CloseElement();
builder.OpenElement(5, "ul");
builder.AddAttribute(6, "class", "list-group");
foreach (string name in data)
{
builder.OpenElement(7, "li");
builder.AddAttribute(8, "class", "list-group-item");
builder.AddContent(9, new MarkupString(name));
builder.CloseElement();
}
builder.CloseElement();
}
}
}
component의 기반 class는 ComponentBase입니다. 일반적으로 HTML요소로서 표현되는 content는 BuildRenderTree method를 재정의하고 RenderTreeBuilder 매개변수를 사용해 생성됩니다. content를 생성하는 것은 각 요소를 여러 줄의 code구문을 사용해 생성하고 설정해야 하므로 다소 번잡해 보일 수 있고 또한 compiler가 code와 content를 일치시키기 위해 사용하는 순번을 가져야 합니다. 순서적으로는 우선 OpenElement method로 AddElement와 AddContent method를 사용해 설정되고 CloseElement method를 통해 완료가 되는 새로운 요소를 시작합니다. 통상 Razor component에서 .razor file에서 문자 그대로 정의된 것처럼 요소에 attribute를 추가함으로써 설정되는 event와 binding을 포함해 가능한 모든 기능을 사용할 수 있습니다. 위 예제에서의 component는 정렬된 name을 button요소가 click 될 때 변경된 정렬방법을 통해 표시하도록 하고 있습니다. 아래 예제에서는 Pages folder의 Blazor.cshtml file에서 사용자에게 표시될 새로운 component를 적용하고 있습니다.
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Class Only</h4>
<component type="typeof(MyBlazorApp.Blazor.CodeOnly)" render-mode="Server" />
Project를 실행하고 /pages/blazor URL을 요청하여 class기반의 Razor component에서 생성한 content를 확인합니다. 이 상태에서 button을 click 하게 되면 list에 있는 name의 정렬 방법이 바뀌게 될 것입니다.