ASP.NET Core - [Blazor] 3. Blazor Server 2
계속해서 이전 글에 이어 Razor Component를 결합하여 사용하는 방식에 중점을 두고 Blazor Server의 나머지 부분에 관해 살펴보고자 합니다.
1. Project 준비하기
예제 project는 이전글에서 사용한 project를 그대로 사용할 것이며 여기에서 더 필요한 변경사항은 없습니다. project를 실행하고 /controllers와 /pages/blazor URL을 순서대로 요청하여 다음과 같은 응답이 생성되는지 확인합니다.
2. Component 결합
Blazor component는 다소 복잡한 기능이 구현되는 경우 서로 결합될 수도 있는데 이런 상황에서 여러 component를 어떻게 결합하고 서로 연결시킬 수 있는지를 알아둘 필요가 있습니다. 우선 Blazor folder에 SelectFilter.razor이라는 이름의 Razor Component를 아래와 같이 추가합니다.
<div class="form-group">
<label for="select-@Title">@Title</label>
<select name="select-@Title" class="form-control" @bind="SelectedValue">
<option disabled selected value="">Select @Title</option>
@foreach (string val in Values)
{
<option value="@val" selected="@(val == SelectedValue)">
@val
</option>
}
</select>
</div>
@code {
public IEnumerable<string> Values { get; set; } = Enumerable.Empty<string>();
public string? SelectedValue { get; set; }
public string Title { get; set; } = "Placeholder";
}
위 예제는 사용자가 선택할 수 있는 select 요소를 render하고 있는 것으로 Blazor folder의 ProductList.razor에서 기존의 select요소를 바꿔 위 예제의 SelectFilter component를 적용합니다.
</table>
<SelectFilter />
@code {
component는 render된 content로 controller view나 Razor page에 의해 추가될 때 component요소가 사용됩니다. 다른 component에 의해 render된 content로 component가 추가될 때 component의 이름을 요소로서 사용하게 됩니다. 이 경우 예제에서는 SelectFilter component를 ProductList component에 의해 render된 content로 추가하였습니다. 또한 이때는 정확하게 대소문자를 구분하여 사용해야 합니다.
component를 결합할때 하나의 component는 layout의 일부에 대한 책임을 다른 component에 위임 하는 효과가 있습니다. 이 경우 ProductList component에서 사용자에게 Manufacturer를 선택하는데 사용되는 select 요소를 제거하였으며 이를 같은 기능을 제공하는 SelectFilter component로 교체하였습니다. component는 부모-자식(상위-하위) 간 관계를 형성하게 되며 PeopleList component는 상위(부모), SelectFilter component는 하위(자식)가 됩니다.
좀 더 적절히 이들 요소를 통합하기 위해 몇몇 추가적인 작업을 해줄 수 있지만 우선은 SelectFilter요소를 추가하고 project를 실행하여 /controllers를 요청함으로써 SelectFilter component가 표시되는 것을 다음과 같이 확인하도록 합니다.
(1) Attribute를 통한 Component 설정
현재 SelectFilter component의 목표는 application전역에서 해당 component가 사용되는 곳마다 값을 표시하도록 설정함으로서 이를 범용으로 사용할 수 있도록 하는 것입니다. Razor Component는 이들을 적용하는 HTML요소에 추가된 attribute를 사용하여 구성되며 HTML요소의 attribute에 할당된 값은 component의 C#속성으로 할당됩니다. 매개변수 attribute는 아래와 같이 Blazor folder의 SelectFilter.razor에서 설정가능한 속성을 정의한 것처럼 component가 설정할 수 있는 C#속성으로 적용됩니다.
@code {
[Parameter]
public IEnumerable<string> Values { get; set; } = Enumerable.Empty<string>();
public string? SelectedValue { get; set; }
[Parameter]
public string Title { get; set; } = "Placeholder";
}
Component는 설정가능한 속성을 선택적으로 적용할 수 있습니다. 예제의 경우 Parameter attribute는 SelectFilter component에 정의된 속성중 2개에 적용되었습니다. 아래 예제는 ProductList가 SelectFilter component를 적용하기 위해 사용한 요소를 변경하여 설정 attribute를 추가한 것입니다.
<SelectFilter values="@Manufacturer" title="Manufacturer" />
설정될 각 속성을 위해 같은 이름의 attribute가 상위의 요소로 추가되었습니다. attribute의 값은 title attribute에 할당된 anufacturer와 같이 고정값을 설정하거나 @Manufacturer처럼 Razor 표현식을 통해 Manufacturer 속성으로 부터의 개체에 대한 배열을 attribute의 값으로 할당할 수 있습니다.
EditorRequired atttribute는 Parameter attribute함께 적용하여 해당 속성에 대한 값이 필수임을 나타낼 수 있습니다. 만약 component가 required attribute 없이 사용되었다면 경고 message가 나타날 수 있습니다
● 다수의 설정을 적용하고 설정된 값을 받기
값을 수신받기 위해 개별적으로 속성을 정의하는 것은 많은 구성설정을 다루어야 하는 경우에 오류를 발생시키기 쉽습니다. 특히 이들 값이 component에 수신되고 하위 component나 HTML요소로 전달할 수 있는 경우에는 더욱 그렇습니다. 하지만 다행스럽게도 단 하나의 속성에 다른 속성과 일치하지 않는 모든 속성의 값을 수신할 수 있도록 지정할 수 있으며 이 값은 Blazor folder의 SelectFilter.razor file에서 적용된 것처럼 설정으로서 적용할 수 있습니다.
<div class="form-group">
<label for="select-@Title">@Title</label>
<select name="select-@Title" class="form-control" @bind="SelectedValue" @attributes="Attrs">
<option disabled selected value="">Select @Title</option>
@foreach (string val in Values)
{
<option value="@val" selected="@(val == SelectedValue)">
@val
</option>
}
</select>
</div>
@code {
[Parameter]
public IEnumerable<string> Values { get; set; } = Enumerable.Empty<string>();
public string? SelectedValue { get; set; }
[Parameter]
public string Title { get; set; } = "Placeholder";
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? Attrs { get; set; }
}
예제에서는 Parameter attribute의 CaptureUnmatchedValues인수에 true를 설정함으로서 해당 속성이 일치하지 않는 attribute를 위한 포괄적인 attribute로서 식별되도록 하였습니다. 또한 속성의 type은 Dictionary<string, object>형으로 지정하여 attribute의 이름과 값이 같이 표현될 수 있도록 해야 하고 type의 속성은 @attribute표현식을 통해 적용되어야 합니다.
이러한 방법은 attribute splatting이라고 하는 것으로 일련의 속성들을 한번에 적용할 수 있도록 하는 것입니다. 위 예제에서의 변경으로 인해 SelectFilter component는 Values와 Title attribute값 이외에 다른 attribute들을 모두 Attrs속성으로 받아 select요소에 전달할 것입니다. 아래 예제에서는 위에서 설명한 효과를 확인해 보기 위해 Blazor folder의 ProductList.razor file에서 몇몇 속성을 추가하였습니다.
<SelectFilter values="@Manufacturer" title="Manufacturer" autofocus="true" name="Manufacturer" required="true" />
project를 실행하고 /controllers로 URL을 요청합니다. select요소로 전달된 attribute들은 겉으로는 영향을 주지 않지만 select요소를 mouse오른쪽 button으로 click 하고 pop-up menu에서 inspect를 선택하게 되면 ProductList component에 있는 SelectFilter요소에 추가된 attribute들이 SelectFilter component에 의해 render된 요소로 추가되어 있음을 확인할 수 있습니다.
<select class="form-control" autofocus="true" name="Manufacturer" required="true">
<option disabled="" selected="" value="">Select Manufacturer</option>
<option value="SAM ET">SAM ET</option>
<option value="HY IC">HY IC</option>
<option value="INT">INT</option>
<option value="CUS">CUS</option>
</select>
● Controller View 또는 Razor Page에서의 Component 설정
Attribute는 또한 component요소를 사용하여 component를 적용할때 이들을 설정하는 데에도 사용할 수 있습니다. 아래 예제는 Blazor folder의 ProductList.razor file에 설정속성을 추가한 것으로 각각은 ProductList component에 database로부터 얼마만큼의 item이 표시될지와 SelectFilter component로 전달될 문자열값에 관한 속성에 관한 것입니다.
<SelectFilter values="@Manufacturer" title="@SelectTitle" />
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Product>? Product => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer).Take(ItemCount);
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" : "";
[Parameter]
public int ItemCount { get; set; } = 4;
[Parameter]
public string? SelectTitle { get; set; }
}
C#속성의 값은 요소에 속성이름을 붙여 이름이 param-으로 시작하는 attribute를 추가함으로서 제공됩니다. 아래 예제는 Views/Home folder의 Index.cshtml을 변경한 것으로 해당 설정속성을 component에 추가하였습니다.
@model ProductListViewModel
@{
}
<h4 class="bg-primary text-white text-center p-2">Product</h4>
<component type="typeof(MyBlazorApp.Blazor.ProductList)" render-mode="Server" param-itemcount="5" param-selecttitle="@("Manufactorers List")" />
param-itemcount attribute는 ItemCount 속성에 대한 값을 제공하며 param-selecttitle attribute는 SelectTitle속성에 대한 값을 제공합니다.
component요소를 사용할때 숫자나 bool형식으로 분석될 수 있는 attribute값은 Razor표현식이 아닌 literal값으로 처리되며 따라서 ItemCount속성에 예제와 같이 4의 값을 지정하였습니다. 그 외 다른 값은 @접미사를 붙이지 않아도 literal값이 아닌 Razor표현식으로 가정하여 처리됩니다. 다소 이상하지만 SelectTitle속성의 값을 특정한 문자열로서 지정하고자 하기 때문에 위 예제와 같이 Razor표현식을 동반하여 특정한 문자열을 전달하는 것입니다.
위와 같이 수정한 후 /controllers로 URL을 호출하여 다음과 같은 응답이 생성되는지 확인합니다.
(2) 사용자정의 Event와 Binding생성
SelectFilter component는 상위 component로 부터 data값을 수신하지만 사용자가 선택할 때 이를 알려줄 방법이 없습니다. 따라서 상위 component가 일반 HTML요소에서 발생되는 Event처럼 처리 method로 등록할 수 있는 사용자정의 Event를 생성해야 합니다. 아래 예제는 Blazor folder의 SelectFilter.razor file에서 Event를 생성한 것으로 SelectFilter component에 event를 추가한 것입니다.
<div class="form-group">
<label for="select-@Title">@Title</label>
<select name="select-@Title" class="form-control" @onchange="HandleSelect" value="@SelectedValue">
<option disabled selected value="">Select @Title</option>
@foreach (string val in Values)
{
<option value="@val" selected="@(val == SelectedValue)">
@val
</option>
}
</select>
</div>
@code {
[Parameter]
public IEnumerable<string> Values { get; set; } = Enumerable.Empty<string>();
public string? SelectedValue { get; set; }
[Parameter]
public string Title { get; set; } = "Placeholder";
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? Attrs { get; set; }
[Parameter]
public EventCallback<string> CustomEvent { get; set; }
public async Task HandleSelect(ChangeEventArgs e)
{
SelectedValue = e.Value as string;
await CustomEvent.InvokeAsync(SelectedValue);
}
}
사용자 정의 event는 예제에서처럼 EventCallback<T>형식의 속성을 추가함으로써 정의됩니다. 이때 generic type 인수는 상위 event handler에 의해 수신될 type이며 이 경우 문자열(string) 형이 됩니다. 또한 select요소를 변경하여 select요소가 onchange event를 trigger 할 때 HandleSelect method를 등록하고 있는 @onchange attribute를 추가하였습니다.
HandleSelect method는 SelectedValue속성을 update하고 EventCallback<T>.InvokeAsync method를 호출함으로써 사용자정의 event를 trigger하고 있습니다.
InvokeAsync method의 인수는 select요소로 부터 수신된 ChangeEventArgs개체로 부터 전달된 값을 사용하여 event를 trigger하는데 사용됩니다. 아래 예제에서는 Blazor folder의 ProductList.razor file을 변경하여 SelectList component로부터 발동된 사용자정의 event를 수신하도록 하고 있습니다.
<SelectFilter values="@Manufacturer" title="@SelectTitle" CustomEvent="@HandleCustom" />
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Product>? Product => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer).Take(ItemCount);
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" : "";
[Parameter]
public int ItemCount { get; set; } = 4;
[Parameter]
public string? SelectTitle { get; set; }
public void HandleCustom(string newValue)
{
SelectedManufacturer = newValue;
}
}
event handler를 설정하기 위해 자식 compoennt를 적용하는 요소에 EventCallback<T> property attribute를 추가했습니다. 이때 attribute의 값은 type T에 대한 매개변수를 수신하는 method를 선택하기 위한 Razor 표현식이 됩니다.
project를 실행하고 /controllers로 URL을 요청한뒤 manufacturers의 list로부터 값을 선택합니다. 사용자 정의 event는 상위 component와 하위 component 간 관계를 완료하고 상위는 하위에 title과 사용자에게 표시할 data값 list를 지정하는 attribute를 통해 설정합니다. 하위요소에서는 상위요소에게 사용자가 선택한 값을 알리기 위해 사용자정의 event를 사용합니다. 이로서 부모요소는 HTML table에서 해당하는 row를 강조하게 됩니다.
● 사용자 Binding 생성
상위 component에서는 하나는 data값이 할당되고 다른 하나는 사용자정의 event가 할당된 속성이 쌍으로 정의된 경우 하위 component에 대한 binding을 생성할 수 있습니다. 여기서 속성의 이름은 중요한데 event속성의 이름은 반드시 data속성에 'Changed'단어를 더한 값과 같아야 합니다.
아래 예제에서는 Blazor folder의 SelectFilter.razor file을 변경한 것으로 binding에 필요한 속성을 확인해 볼 수 있습니다.
[Parameter]
public string? SelectedValue { get; set; }
[Parameter]
public string Title { get; set; } = "Placeholder";
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? Attrs { get; set; }
[Parameter]
public EventCallback<string> SelectedValueChanged { get; set; }
public async Task HandleSelect(ChangeEventArgs e)
{
SelectedValue = e.Value as string;
await SelectedValueChanged.InvokeAsync(SelectedValue);
}
Paramter attribute가 SelectedValue와 SelectedValueChanged속성모두에 적용되어야 한다는 점에 주목해야 합니다. 이들 attribute가 생략되면 data binding은 예상한 대로 동작하지 않습니다.
상위 component에서는 하위 component로 @bind-[이름] attribute를 통해 bind하게 되는데 여기서 [이름]은 하위 component에서 정의된 속성에 해당합니다. 예제에서는 하위 component 속성의 이름은 SelectedValue이며 따라서 상위 component에서는 Blazor folder의 변경된 ProductList.razor file에서와 같이 @bind-SelectedValue를 사용해 binding을 생성할 수 있습니다.
<SelectFilter values="@Manufacturer" title="@SelectTitle" @bind-SelectedValue="SelectedManufacturer" />
<button class="btn btn-primary mt-2" @onclick="@(() => SelectedManufacturer = "INT")">
Change
</button>
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Product>? Product => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer).Take(ItemCount);
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" : "";
[Parameter]
public int ItemCount { get; set; } = 4;
[Parameter]
public string? SelectTitle { get; set; }
//public void HandleCustom(string newValue)
//{
// SelectedManufacturer = newValue;
//}
}
project를 실행하고 /controllers로 URL을 요청한뒤 Manufacturer list로부터 SAM ET 선택합니다. 사용자정의 binding은 select요소에서 선택된 값을 table에서 강조표시가 되도록 반영하게 됩니다.
그리고 다른 방향으로 binding을 test하기 위해 Change button을 click 하면 다음과 같이 변경된 manufacturer가 강조표시됨을 확인할 수 있습니다.
3. Component안에서 하위 content 표시하기
하위 content를 표시하는 Component는 상위에서 제공되는 요소를 감싸는 것으로 동작합니다. 하위 요소의 content를 어떻게 관리할 수 있는지를 확인해 보기 위해 Blazor folder에 ThemeWrapper.razor이름의 Razor Component를 아래와 같이 추가합니다.
<div class="p-2 bg-@Theme border text-white">
<h5 class="text-center">@Title</h5>
@ChildContent
</div>
@code {
[Parameter]
public string? Theme { get; set; }
[Parameter]
public string? Title { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
하위 content를 수신받기 위해 component에서는 type이 RenderFragment ChildContent라는 이름의 속성을 정의하고 있으며 해당 속성에 Parameter attribute로 적용하고 있습니다. 예제에서의 component는 childcontent를 div요소 안으로 감싸고 있으며 div는 Bootstrap theme color를 사용하고 하위요소에서 title을 표시하고 있습니다. theme color의 이름과 title의 text는 또한 paramter로서 수신받습니다.
요소의 재사용 제한
사용자에게 표시된 content를 update하는 경우 Blazor는 요소를 새로 생성하는 동작에서 더 많은 비용이 들어가므로 가능하다면 요소를 재사용하게 됩니다. 특히 이러한 동작은 @for나 @foreach 표현식 등을 통해 값의 배열에서 다수의 요소를 표시해야 하는 경우 성능적으로 유리하기도 합니다. 그래서 만약 배열에서 변경사항이 발생하면 Blazor는 이전 data값으로 생성된 요소를 재사용하여 새로운 data를 표시할 것입니다.
하지만 이러한 동작은 Blazor의 관리밖에 있는(사용자정의 JavaScript code와 같은) 요소를 변경한 경우 문제가 생길 수도 있습니다. Blazor는 이러한 변경사항을 알지 못하고 따라서 요소의 재사용을 계속 시도하게 됩니다. 비록 이러한 상황이 흔한 것은 아니지만 아래와 같이 @key attribute를 사용하고 배열에서의 data값 중 하나를 요소와 연결시키는 표현식을 제공함으로써 요소가 재사용되는 걸 방지할 수 있습니다.
<tr @key="p.ProductId" class="@GetClass(p.ProductManufacturer?.ManufacturerName)">
Blazor는 같은 key를 가진 data항목이 있는 경우에만 요소를 재사용할 것이며 다른 값의 경우에는 요소를 새롭게 생성할 것입니다.
하위 content는 Blazor folder의 아래 ProductList.razor에서처럼 component를 적용할때 시작과 종료 tag사이에 HTML요소를 추가함으로써 정의됩니다.
<ThemeWrapper Theme="info" Title="Location Selector">
<SelectFilter values="@Manufacturer" title="@SelectTitle" @bind-SelectedValue="SelectedManufacturer" />
<button class="btn btn-primary mt-2" @onclick="@(() => SelectedManufacturer = "INT")">
Change
</button>
</ThemeWrapper>
하위 content를 설정하기위해 필요한 추가적인 attribute는 없으며 ChildContent속성으로 자동으로 할당되고 처리될 것입니다. ThemeWrapper component가 어떻게 하위 content를 표시하는지를 확인하기 위해 project를 실행하고 /controllers URL을 요청합니다. 그러면 theme를 선택한 설정 attribute와 응답을 생성하기 위해 사용된 title text를 다음과 같이 볼 수 있을 것입니다.
(1) Template Component 생성하기
Template component는 표시되어야 할 여러 영역에 따라 하위 content를 표현하기 위한 더 구조적인 방안을 제공해 줄 수 있습니다. Template component는 또한 application전역에서 사용되는 기능을 통합하기 위한 가장 좋은 방법으로 code와 content가 중복되는것을 방지할 수 있습니다.
Blazor folder에 TableTemplate.razor라는 Razor Component를 아래와 같이 추가합니다.
<table class="table table-sm table-bordered table-striped">
@if (Header != null)
{
<thead>@Header</thead>
}
<tbody>@Body</tbody>
</table>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
}
예제 component에서는 하위 content에 대해 table의 각 content영역을 표시하는 Header와 Body라는 이름의 RenderFragment 속성을 2개로 정의하고 있습니다. 하위 content에 대한 각 영역은 @Header와 @Body 표현식을 통해 render 되며 속성의 값이 null인지를 확인함으로써 content가 특정영역을 위해 제공되는지의 여부를 확인할 수 있습니다.
template component를 사용할때 아래 Blazor folder의 ProductList.razor file에서 처럼 각 영역에 대한 content는 tag가 해당 RenderFragment속성의 이름과 일치하는 HTML요소로 둘러싸서 처리합니다.
<TableTemplate>
<Header>
<tr>
<th>ID</th>
<th>Name (Price)</th>
<th>Category</th>
<th>Manufacturer</th>
</tr>
</Header>
<Body>
@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>
}
</Body>
</TableTemplate>
하위 content는 Header와 Body인 template component의 속성에 해당하는 영역으로 구조화되었고 이로 인해 table구조를 담당하는 TableTemplate component와 상세제공을 담당하는 ProductList component를 남기게 됩니다. project를 실행하고 /controllers로 URL을 요청합니다. 그러면 다음과 같이 template component에 의해 생성된 결과를 확인할 수 있습니다.
(2) Template Component에서 Generic Type 매개변수 사용하기
이전 예제에서 만든 template component는 application전체에서 사용할 수 있는 table을 일관성있게 표현할 수 있다는 점에서 유용할 수 있지만 다른 한편으로는 table body의 row를 생성하는데 직접적인 역할을 하는 상위 component에 의존적인 상황이므로 어느 정도 제한이 있을 수밖에 없습니다. template component는 표현하는 content에 대한 어떠한 이해도 없으므로 content를 표시하는 것 외에는 어떠한 동작도 수행할 수 없습니다.
Template component는 generic type 매개변수를 사용하여 data를 인식하는 것으로 만들 수 있는데 이렇게 하면 상위 component에서 일련의 data개체를 제공할 수 있고 template에서는 이것을 받아 표시할 수 있습니다. 따라서 template component는 결과적으로 각 data개체에 대한 content를 생성할 수 있게 되고 더 유용한 기능들을 제공할 수 있게 됩니다. 설명한 바와 같이 해당 접근방식을 통해 몇 개의 table row가 표시되는지와 table row를 직접 선택하기 위한 기능을 template component에 추가하고자 하며 그 첫 번째 절차로 우선 아래와 같이 Blazor folder의 TableTemplate.razor file에서 처럼 component에 generic type 매개변수를 추가하여 table body에 content를 render 할 수 있도록 합니다.
@typeparam RowType
<table class="table table-sm table-bordered table-striped">
@if (Header != null)
{
<thead>@Header</thead>
}
<tbody>
@if (RowData != null && RowTemplate != null)
{
@foreach (RowType item in RowData)
{
<tr>@RowTemplate(item)</tr>
}
}
</tbody>
</table>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment<RowType>? RowTemplate { get; set; }
[Parameter]
public IEnumerable<RowType>? RowData { get; set; }
}
generic type 매개변수는 @typeparam attribute를 사용해 지정되며 예제의 경우 component가 table row를 생성할 data type을 참조하는 RowType이라는 이름의 매개변수를 부여하였습니다.
Blazor generic type 매개변수는 C# where keyword사용을 제한할 수 있으며 이로 인해 particular interface를 구현하는 class와 같이 지정된 특성만을 가진 type만이 사용될 수 있습니다. 자세한 사항은 아래 글을 참고하시기 바랍니다.
where (generic type constraint) - C# Reference | Microsoft Learn
component가 처리할 data는 generic type에 대한 일련의 개체에 해당하는 type속성을 추가함으로서 수신됩니다. 예제에서 속성의 이름은 RowData이며 type은 IEnumerable<RowType>입니다. component의 content는 RenderFragment<T>속성을 사용해 수신된 각 개체를 표시할 것입니다. 예제에서는 generic type 매개변수를 선택한 이름을 반영하는 RenderFragment<RowType>형식인 RowTemplate라는 속성을 정의하고 있습니다.
component가 RenderFragment<T>속성을 통해 content section을 수신할 때 method로서 section을 호출하고 인수로 개체를 사용함으로써 단일 개체를 다음과 같이 render 할 수 있습니다.
@foreach (RowType item in RowData)
{
<tr>@RowTemplate(item)</tr>
}
위 code조각은 RowData collection에서 열거에서 RowType개체를 열거 하고 각각에서 RowTemplate속성을 통해 수신된 content section을 render 하고 있습니다.
● Generic Template Component 사용
아래 예제는 Blazor folder의 ProductList.razor file을 변경한 것으로 이전의 기능을 삭제하고 Product개체의 table을 생성하기 위해 Generic Template Component를 사용하도록 하였습니다.
<TableTemplate RowType="Product" RowData="Products">
<Header>
<tr>
<th>ID</th>
<th>Name (Price)</th>
<th>Category</th>
<th>Manufacturer</th>
</tr>
</Header>
<RowTemplate Context="p">
<td>@p.ProductId</td>
<td>@p.ProductName, @p.ProductPrice?.ToString("#,##0")</td>
<td>@p.ProductCategory.CategoryName</td>
<td>@p.ProductManufacturer?.ManufacturerName</td>
</RowTemplate>
</TableTemplate>
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Product>? Products => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer);
}
RowType attribute는 generic type인수의 값을 지정하는데 사용되며 RowData attribute는 template component가 처리할 data를 지정합니다. 또한 RowTemplate요소는 각 data개체에서 생성되는 요소를 표시합니다. RenderFragment<T>속성으로 content section을 정의할 때 Context attribute는 현재개체에 대한 이름으로 'p'를 할당하는 데 사용하고 있으며 이를 통해 Razor 표현식으로 content section의 요소를 채우는 데 사용됩니다.
예제에서의 전반적인 효과는 template component가 Product개체를 표시하기 위해 구성되었다는 것입니다. component는 각 Product에 대한 table row를 생성할 것이며 여기에는 현재 Product개체의 속성을 사용해 설정되는 td요소를 포함할 것입니다.
또한 예제에서는 Parameter가 적용된 attribute를 제거하였기 때문에 Views/Home folder의 Index.cshtml에서도 아래와 같이 ProductList component를 적용하는 요소로 부터 해당 attribute를 같이 제거해야 합니다.
<component type="typeof(MyBlazorApp.Blazor.ProductList)" render-mode="Server" />
적용된 generic template component를 확인해 보기 위해 project를 실행하고 /controllers URL을 요청합니다. ProductList에서 제공하는 data와 content section은 TableTemplate component에서 table을 생성하기 위해 다음과 같이 사용될 것입니다.
● Generic Template Component에 기능 추가하기
아래 예제는 Blazor folder의 TableTemplate.razor file에 기능을 추가한 것으로 template component에 data를 다룰 수 있는 통로를 제공하면 기능추가에 대한 기반을 마련할 수 있게 됩니다.
@typeparam RowType
<div class="container-fluid">
<div class="row p-2">
<div class="col">
<SelectFilter Title="@("Sort")" Values="@SortDirectionChoices" @bind-SelectedValue="SortDirectionSelection" />
</div>
<div class="col">
<SelectFilter Title="@("Highlight")" Values="@HighlightChoices()" @bind-SelectedValue="HighlightSelection" />
</div>
</div>
</div>
<table class="table table-sm table-bordered table-striped">
@if (Header != null)
{
<thead>@Header</thead>
}
<tbody>
@if (RowTemplate != null)
{
@foreach (RowType item in SortedData())
{
<tr class="@IsHighlighted(item)">@RowTemplate(item)</tr>
}
}
</tbody>
</table>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment<RowType>? RowTemplate { get; set; }
[Parameter]
public IEnumerable<RowType> RowData { get; set; } = Enumerable.Empty<RowType>();
[Parameter]
public Func<RowType, string> Highlight { get; set; } = (row) => String.Empty;
public IEnumerable<string> HighlightChoices() => RowData.Select(item => Highlight(item)).Distinct();
public string? HighlightSelection { get; set; }
public string IsHighlighted(RowType item) => Highlight(item) == HighlightSelection ? "table-dark text-white" : string.Empty;
[Parameter]
public Func<RowType, string> SortDirection { get; set; } = (row) => String.Empty;
public string[] SortDirectionChoices = new string[] { "Ascending", "Descending" };
public string SortDirectionSelection { get; set; } = "Ascending";
public IEnumerable<RowType> SortedData() => SortDirectionSelection == "Ascending" ? RowData.OrderBy(SortDirection) : RowData.OrderByDescending(SortDirection);
}
예제에서는 이전에 생성된 SelectFilter component를 통해 2개의 select요소를 사용자에게 제공하고 있습니다. 이들 새로운 요소는 사용자가 data를 오름차순이나 내림차순으로 정렬할 수 있게 하고 table에서 row를 강조하기 위해 사용된 값을 선택할 수 있습니다. 아래 예제는 Blazor folder의 ProductList.razor file을 변경한 것으로 상위 component에서는 template component에 정렬과 강조를 위해 사용된 속성을 선택할 수 있는 기능을 주는 추가적인 매개변수를 제공합니다.
<TableTemplate RowType="Product" RowData="Products" Highlight="@(p => p.ProductManufacturer.ManufacturerInc)" SortDirection="@(p => p.ProductName)">
Highlight attribute는 template component에 table의 row를 강조하기 위해 사용된 속성을 선택하는 기능을 제공하고 있으며 SortDirection attribute는 정렬을 위해 사용된 속성을 선택할 수 있는 기능을 제공하고 있습니다. 위 예제의 결과를 확인하기 위해 /controllers URL을 요청하고 다음과 같이 새로운 select요소가 포함된 응답이 생성되는지 확인합니다. 이 select요소들로 인해 사용자는 목록의 정렬순서와 강조처리항목을 다음과 같이 변경할 수 있습니다.
● Generic Template Component 재사용
template component로 추가된 기능은 모두 generic type 매개변수에 의존하고 있으며 이를 통해 component가 표현하는 content를 특정 class와 연결시키지 않고 수정할 수 있습니다. 결과적으로 component는 table이 필요한 모든 data type에 대해 화면을 표시하고 정렬하고 강조하는 데 사용될 수 있습니다. Blazor folder에 CategoryList.razor이름의 file을 아래와 같이 추가합니다.
<TableTemplate RowType="Category" RowData="Categories" Highlight="@(d => d.CategoryName)" SortDirection="@(d => d.CategoryName)">
<Header>
<tr><th>ID</th><th>Name</th><th>Product</th></tr>
</Header>
<RowTemplate Context="c">
<td>@c.CategoryId</td>
<td>@c.CategoryName</td>
<td>@(String.Join(", ", c.Product.Select(p => p.ProductName)))</td>
</RowTemplate>
</TableTemplate>
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Category>? Categories => Context?.Category?.Include(d => d.Product!);
}
예제에서 TableTemplate component는 database에 있는 Category목록과 함께 Entity Framework Core Include method로 질의된 관련 ProductName을 표시하고 있습니다. 그리고 아래 예제는 Pages folder의 Blazor.cshtml file을 변경하여 표시될 Razor component를 CategoryList로 변경하였습니다.
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Category</h4>
<component type="typeof(MyBlazorApp.Blazor.CategoryList)" render-mode="Server" />
project를 실행하여 /pages/blazor로 URL을 요청하면 template component를 사용하여 다음과 같은 응답이 표시될 것입니다.
(3) 단계적 매개변수
component의 수가 증가함에 따라 component의 계증구조에서 하위 component에 설정 data를 제공하는 것이 필요할 수 있으며 이것은 서로 연결되어 있는 각 component에서 data를 수신하고 모든 하위에 전달하는 방법으로 수행할 수 있습니다. 하지만 이러한 방법은 error를 일으키기 쉽고 심지어 하위 중 어떤 것도 전달된 data를 사용하지 않는 경우라 해도 모든 component가 해당 처리를 수행해야 한다는 문제가 있습니다.
다행스럽게도 Blazor는 component가 중간 component의 중계없이 모든 하위 component에서 직접적으로 사용가능한 data값을 제공하는 이른바 단계적 매개변수를 지원함으로써 이러한 문제를 해결할 수 있습니다. 단계적 매개변수는 content의 영역을 감싸는 데 사용되는 CascadingValue component를 통해 Blazor folder의 CategoryList file에 대한 아래 예제에서 처럼 정의할 수 있습니다.
<CascadingValue Name="BgTheme" Value="Theme" IsFixed="false" >
<TableTemplate RowType="Category" RowData="Categories" Highlight="@(d => d.CategoryName)" SortDirection="@(d => d.CategoryName)">
<Header>
<tr><th>ID</th><th>Name</th><th>Product</th></tr>
</Header>
<RowTemplate Context="c">
<td>@c.CategoryId</td>
<td>@c.CategoryName</td>
<td>@(String.Join(", ", c.Product.Select(p => p.ProductName)))</td>
</RowTemplate>
</TableTemplate>
</CascadingValue>
<SelectFilter Title="@("Theme")" Values="Themes" @bind-SelectedValue="Theme" />
@code {
[Inject]
public BlazorTDBContext? Context { get; set; }
public IEnumerable<Category>? Categories => Context?.Category?.Include(d => d.Product!);
public string Theme { get; set; } = "info";
public string[] Themes = new string[] { "primary", "info", "success" };
}
CascadingValue요소는 해당 요소가 둘러싸는 component와 그 하위에서 값을 사용할 수 있도록 합니다. 여기서 Name attribute는 매개변수의 이름을 지정하는 것이며 Value attribute는 값을 지정하고 IsFixed attribute는 값이 변경되는지에 대한 여부를 지정하는 데 사용됩니다. CascadingValue요소는 예제에서 'BgTheme'이름의 단계적 매개변수를 생성하기 위해 사용되었으며 해당 매개변수의 값은 사용자에게 Bootstrap CSS theme를 선택할 수 있도록 하는 SelectFilter component의 instance에 의해 설정됩니다.
각 CascadingValue요소는 단계적 매개변수 하나를 생성합니다. 만약 여러값을 전달해야 한다면 CascadingValue을 중첩하거나 dictionary를 통해 여러 설정을 제공하는 단일 매개변수를 생성할 수 있습니다.
단계적 매개변수는 CascadingParameter를 통해 단계적 매개변수를 필요로하는 component에서 아래 Blazor folder의 SelectFilter.razor file 예제에서와 같이 직접적으로 수신됩니다.
<div class="form-group p-2 bg-@Theme @TextColor()">
<label for="select-@Title">@Title</label>
<select name="select-@Title" class="form-control" @onchange="HandleSelect" value="@SelectedValue">
<option disabled selected value="">Select @Title</option>
@foreach (string val in Values)
{
<option value="@val" selected="@(val == SelectedValue)">
@val
</option>
}
</select>
</div>
@code {
[Parameter]
public IEnumerable<string> Values { get; set; } = Enumerable.Empty<string>();
[Parameter]
public string? SelectedValue { get; set; }
[Parameter]
public string Title { get; set; } = "Placeholder";
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? Attrs { get; set; }
[Parameter]
public EventCallback<string> SelectedValueChanged { get; set; }
public async Task HandleSelect(ChangeEventArgs e)
{
SelectedValue = e.Value as string;
await SelectedValueChanged.InvokeAsync(SelectedValue);
}
[CascadingParameter(Name = "BgTheme")]
public string Theme { get; set; } = string.Empty;
public string TextColor() => String.IsNullOrEmpty(Theme) ? "text-dark" : "text-light";
}
CascadingParameter attribute의 Name매개변수는 cascading 매개변수의 이름을 지정하는 데 사용됩니다. 이전 예제에서 정의된 BgTheme매개변수는 위 예제의 Theme속성으로 수신되며 이를 다시 component의 배경색상을 지정하는 데 사용합니다. project를 실행하여 /pages/blazor URL을 요청하면 아래와 같은 응답이 생성됩니다.
보시는 바와 같이 SelectFilter component에 대한 3개의 instance가 존재하지만 단 2개만이 CascadingValue요소에 의해 포함되는 계층구조 내에 있습니다. 다른 instance는 CascadingValue요소의 외부에 정의되어 있으며 cascading값을 전달받지 않습니다.
4. 오류 처리
이제 마지막으로 Blazor가 제공하는 연결오류와 처리되지 않은 application오류를 다루기 위한 기능을 살펴보겠습니다.
(1) 연결 오류 처리
Blazor는 browser와 ASP.NET Core server사이의 연결을 지속적 HTTP 연결로 의존하고 있습니다. application은 연결이 중단되면 작동할 수 없고 사용자가 application과의 상호작용을 계속 시도하는 것을 방지하기 위해 modal오류 message를 표시하게 됩니다.
위와 같은 방식에 따라 Blazor에서는 아래 Pages folder의 Blazor.cshtml예제에서와 같이 특정한 id의 요소를 정의함으로서 연결오류를 사용자정의할 수 있습니다.
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Category</h4>
<link rel="stylesheet" href="connectionErrors.css" />
<div id="components-reconnect-modal" class="h4 bg-dark text-white text-center my-2 p-2 components-reconnect-hide">
Connection Lost
<div class="reconnect">
Trying to reconnect...
</div>
<div class="failed">
Reconnection Failed.
<button class="btn btn-light btn-sm m-1" onclick="window.Blazor.reconnect()">Reconnect</button>
</div>
<div class="rejected">
Reconnection Rejected.
<button class="btn btn-light btn-sm m-1" onclick="location.reload()">Reload</button>
</div>
</div>
<component type="typeof(MyBlazorApp.Blazor.CategoryList)" render-mode="Server" />
사용자정의 error 요소의 id는 반드시 components-reconnect-model이 되어야 합니다. 여기서 만약 연결 error가 발생한다면 Blazor는 해당 요소를 찾고 아래 표에 해당하는 4개의 CSS class 중 하나를 추가하게 됩니다.
components-reconnect-show | 요소는 연결이 끊어지고 Blazor가 재연결을 시도할때 해당 class에 추가됩니다. 그러면서 error message가 사용자에게 표시되고 Blazor content와의 상호작용은 차단됩니다. |
components-reconnect-hide | 만약 연결이 다시 이루어 진다면 요소는 해당 class에 추가됩니다. 그러면서 error message는 숨겨지고 상호작용이 다시 허가됩니다. |
components-reconnect-failed | Blazor가 재연결에 실패한다면 요소는 해당 class에 추가됩니다. 그러면서 사용자에게는 재연결을 시도하기 위해 window.Blazor.reconnect()를 호출할 수 있는 button이 제공됩니다. |
components-reconnect-rejected | Blazor가 Server에 도달하긴 했으나 사용자의 연결상태가 손실된 경우 요소는 해당 class에 추가됩니다. 이것은 일반적으로 sever가 재실행된 경우 발생하며 application을 reload하기 위해 location.reload()를 호출하는 button이 사용자에게 제공됩니다. |
초기에 요소는 어떠한 class에도 추가되지 않습니다. 때문에 명시적으로 components-reconnect-hide class를 추가했고 따라서 문제가 발생할때까지는 실제 표시되지 않을 것입니다.
예제의 목적은 재연결동안 발생할 수 있는 각 상태에 따라 특정 error message를 사용자에게 제공하고자 하는 것입니다. 이를 위해 각 상태에 따라 error message를 표시하는 요소를 추가하였으며 각 요소의 표시여부를 관리하기 위해 wwwroot folder에 connectionErrors.css라는 이름의 CSS stylesheet file을 아래와 같이 추가하였습니다.
#components-reconnect-modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
overflow: hidden;
opacity: 0.9;
}
.components-reconnect-hide {
display: none;
}
.components-reconnect-show {
display: block;
}
.components-reconnect-show > .reconnect {
display: block;
}
.components-reconnect-show > .failed,
.components-reconnect-show > .rejected {
display: none;
}
.components-reconnect-failed > .failed {
display: block;
}
.components-reconnect-failed > .reconnect,
.components-reconnect-failed > .rejected {
display: none;
}
.components-reconnect-rejected > .rejected {
display: block;
}
.components-reconnect-rejected > .reconnect,
.components-reconnect-rejected > .failed {
display: none;
}
위 style은 components-reconnect-modal요소를 modal item으로서 보여주게 되는데 이때 components-reconnect-hide와 components-reconnect-show class를 통해 가시성을 결정하게 됩니다. 특정 message의 가시성은 위 표에 있는 class의 application에 기반하여 전환됩니다.
project를 실행하고 /pages/blazor로 URL을 요청합니다. component가 표시될때까지 대기한 후 ASP.NET Core server를 중지합니다. 그렇게 되면 Blazor가 재연결을 시도함으로써 표시되는 error message를 보게 될 것이며 잠시 기다리면 재연결이 실패했음을 나타내는 message가 다시 표시됨을 볼 수 있습니다.
browser만으로 연결복구를 test하는건 지속적 HTTP연결을 중단시키기 위한 방법이 없기 때문에 불가능합니다. 대신 Fiddler proxy라는 도구를 사용하면 ASP.NET Core server를 중단시키지 않고 connection을 중단시킬 수 있으므로 이를 대체적으로 사용해 볼 수 있습니다.
(2) 처리되지 않은 오류 다루기
Blazor는 별도로 예외처리하지 않은 예외가 발생하는 application의 오류에는 잘 응답하지 않습니다. 예외가 처리되는 방식을 알아보기 위해 Blazor folder의 SelectFilter.razor file에서와 같이 사용자가 특정한 값을 선택할때 예외가 발생하도록 처리합니다.
public async Task HandleSelect(ChangeEventArgs e)
{
SelectedValue = e.Value as string;
if (SelectedValue == "CPU")
{
throw new Exception("CPU cannot be selected");
}
await SelectedValueChanged.InvokeAsync(SelectedValue);
}
project를 실행한 뒤 /pages/blazor URL을 요청하고 Highlight menu에서 CPU를 선택합니다. browser상에서는 아무런 변화도 발생하지 않을 것이지만 menu에서 CPU를 선택하는 순간에는 server에서 예외가 발생하게 됩니다.(사용자는 여전히 select요소를 사용해 특정 값을 선택할 수 있지만 선택에 대한 event handler는 더 이상 작동하지 않으며 application 역시 응답하지 않게 됩니다.)
처리되지 않은 예외가 발생하는 경우 Blazor는 id가 blazor-error-ui인 요소를 찾고 CSS display속성에 block을 설정합니다. 아래 예제는 Pages folder의 Blazor.cshtml file을 변경한 것으로 유용한 message를 표시하기 위해 해당 id의 요소를 추가하였습니다.
<div id="blazor-error-ui" class="text-center bg-danger h6 text-white p-2 fixed-top w-100" style="display:none">
An error has occurred. This application will not respond until reloaded.
<button class="btn btn-sm btn-primary m-1" onclick="location.reload()">Reload</button>
</div>
<component type="typeof(MyBlazorApp.Blazor.CategoryList)" render-mode="Server" />
해당 요소가 표시되는 경우에는 사용자에게 경고 message와 함께 browser를 reload할 수 있는 button이 제공됩니다. project를 실행하고 /pages/blazor URL을 요청한 뒤 동일하게 menu에서 CPU를 선택합니다. 그러면 다음과 같은 응답이 생성될 것입니다.
(3) Error Boundary 사용
Error Boundary는 component계층구조안에서의 error를 포함하는 데 사용되며 때문에 component는 자체 및 하위 component에서 발생한 예외를 처리하게 됩니다. 아래 예제는 Blazor folder의 TableTemplate.razor file을 변경한 것으로 error boundary를 도입하여 SelectFilter component에서 발생한 예외를 포함할 수 있도록 처리하고 있습니다.
<link rel="stylesheet" href="errorBoundaries.css" />
<div class="container-fluid">
<div class="row p-2">
<div class="col">
<SelectFilter Title="@("Sort")" Values="@SortDirectionChoices" @bind-SelectedValue="SortDirectionSelection" />
</div>
<div class="col">
<ErrorBoundary>
<SelectFilter Title="@("Highlight")" Values="@HighlightChoices()" @bind-SelectedValue="HighlightSelection" />
</ErrorBoundary>
</div>
</div>
</div>
Error boundary는 ErrorBoundary요소를 사용해 정의되며 예외가 발생할때까지 하위 content를 정상적으로 표시합니다. 따라서 만약 예외가 발생한다면 하위 content는 삭제되고 blazor-error-boundary CSS class가 할당된 div요소가 대신 표시하게 됩니다. 표시될 content를 정의하고 CSS를 입히기 위해 wwwroot folder에 errorBoundaries.css이름의 CSS stylesheet를 아래와 같이 추가합니다.
blazor-error-boundary {
background-color: darkred;
color: white;
padding: 1rem;
text-align: center;
vertical-align: middle;
height: 100%;
font-size: large;
font-weight: bold;
}
.blazor-error-boundary::after {
content: "Error: Product selected"
}
error boundary의 동작을 직접 확인해 보기 위해 project를 실행하고 /pages/blazor URL을 요청한 뒤 Highlight menu로부터 CPU를 선택하면 error boundary에 의해 포함된 예외가 발생하게 되고 다음과 같은 응답이 생성됩니다. 보시는 바와 같이 오로지 ErrorBoundary component안에 포함된 content만이 예외에 적용되는 것으로 application의 다른 부분은 정상적으로 작동하게 됩니다. 따라서 사용자는 최소한 정렬기능만큼은 계속 사용할 수 있게 됩니다.
● Boundary안에 Error content 정의하기
CSS stylesheet안에 error message를 정의하는 것은 사실 그다지 좋은 선택이 아닙니다. 대신 Blazor folder의 아래 TableTemplate.razor file에서 처럼 error boundary의 일부로서 error content를 정의할 수 있습니다.
<div class="col">
<ErrorBoundary>
<ChildContent>
<SelectFilter Title="@("Highlight")" Values="@HighlightChoices()" @bind-SelectedValue="HighlightSelection" />
</ChildContent>
<ErrorContent>
<h4 class="bg-danger text-white text-center h-100 p-2">
Inline error: Product Selected
</h4>
</ErrorContent>
</ErrorBoundary>
</div>
ChildContent와 ErrorContent tag는 일반적인 경우와 예외가 발생한 경우에 표시할 content를 지정하는데 사용됩니다. project를 실행하고 /pages/blazor URL을 요청한 뒤 Highlight menu로부터 CPU를 선택하면 다음과 같은 형태의 오류를 확인할 수 있습니다.
● 예외로 부터의 오류
Error boundary는 비록 근본적으로 발생한 문제점이 무엇이든 간에 해결되어야 하겠지만 필요한 경우 application이 예외로부터 복구할 수 있도록 지원할 수 있습니다. 아래 예제는 Blazor folder의 TableTemplate.razor file에서 복구가능한 Error Boundary를 어떻게 적용할 수 있는지를 나타내고 있습니다.
<div class="col">
<ErrorBoundary @ref="boundary">
<ChildContent>
<SelectFilter Title="@("Highlight")" Values="@HighlightChoices()" @bind-SelectedValue="HighlightSelection" />
</ChildContent>
<ErrorContent>
<h4 class="bg-danger text-white text-center h-100 p-2">
Inline error: Product Selected
<div>
<button class="btn btn-light btn-sm m-1" @onclick="@(() => boundary?.Recover())">
Recover
</button>
</div>
</h4>
</ErrorContent>
</ErrorBoundary>
</div>
..생략
@code {
ErrorBoundary? boundary;
@ref은 Recover method를 정의하고 있는 ErrorBoundary에 대한 참조를 가져오는데 사용됩니다. error content는 사용자에게 click시 Recover method를 호출하는 button을 포함한 error content를 표시하고 이를 통해 error로부터 사용자가 복구할 수 있는 방안을 제공해 주게 됩니다.
project를 실행하고 /pages/blazor로 URL을 요청합니다. 그리고 Highlight menu로 부터 CPU를 선택하여 error를 유발합니다. 그런 뒤 다시 Recover button을 click 하면 하위 content가 다음과 같이 다시 표시될 것입니다.