.NET/ASP.NET

ASP.NET Core - [Blazor] 4. 고급 Blazor 기능

클리엘 2023. 3. 7. 11:51
728x90

Blazor는 URL routing을 지원함으로써 여러 component가 단일 요청에서 표시될 수 있습니다. 이번 글에서는 이와 관련된 내용을 다룰 것이며 routing system을 어떻게 설정할지, route를 어떻게 정의할지 그리고 layout에서 공용 content를 어떻게 생성할 수 있는지에 대한 것들도 함께 알아볼 것입니다.

 

또한 Blazor 환경에 component가 능동적으로 참여할 수 있는 component 생명주기에 관해서도 다루어 볼 텐데 이것은 URL routing기능을 사용하기 시작할 때 특히 중요한 부분입니다. 마지막으로 이전글에서 설명한 부모-자식(상위-하위) 간 관계의 외부에서 component가 상호작용할 수 있는 다양한 방법에 관해서도 같이 살펴보고자 합니다.

 

1. Project 준비하기

 

예제를 위해 필요한 Project는 이전글에서 사용하던 Project를 그대로 사용할 것이며 추가적인 작업은 필요하지 않습니다. 이전 project를 실행하고 /controllers와 /blazor URL을 요청하여 이전에 나왔던 응답이 그대로 생성되는지 확인합니다.

 

2. Component Routing 사용

 

Blazor는 ASP.NET Core routing system을 기반으로 하여 사용자에게 표시할 component를 선택하도록 할 수 있으며 이로 인해 application은 다양한 Razor Component를 표시함으로서 URL의 변경에 따른 대응을 수행할 수 있습니다. 해당 부분을 시작해 보기 위해 Blazor folder에 Routed.razor file을 아래와 같이 추가합니다.

<Router AppAssembly="typeof(Program).Assembly">
	<Found>
		<RouteView RouteData="@context" />
	</Found>
	<NotFound>
		<h4 class="bg-danger text-white text-center p-2">
			No Matching Route Found
		</h4>
	</NotFound>
</Router>

Router는 Found와 NotFound 영역을 정의하고 있는 generic template component로서 ASP.NET Core에 포함되며 Blazor와 ASP.NET Core routing 기능사이에 연결을 제공합니다.

 

예제의 Router component는 AppAssembly attribute가 필요한데 이는 사용할 .NET assembly를 지정하는 것으로 대부분의 project에서는 현재 assembly를 다음과 같이 지정할 수 있습니다.

<Router AppAssembly="typeof(Program).Assembly">

또한 Router component의 Found속성의 type은 RenderFragment<RouteData>가 되며 값은 RouteData 속성을 통해 RouteView component를 다음과 같이 전달하고 있습니다.

<Found>
	<RouteView RouteData="@context" />
</Found>

RouteView component는 현재 route와 일치하는 component를 표시하는 역활을 하며, 잠깐 언급한 바와 같이 layout을 통해 공용 content를 표시합니다. NotFound 속성의 type은 generic type 인수가 없는 RenderFragment이며 현재 route에 해당하는 component를 찾을 수 없는 경우의 content영역을 표시합니다.

 

(1) Razor Page 준비

 

각각의 component는 이전 글에서 본 것처럼 controller view나 Razor Page에서 표시될 수 있지만 component routing을 사용할 때는 지원되는 URL방식이 제한적이고 유지관리의 어려움으로 이어질 수 있기 때문에 Blazor와의 작업을 구별하는 일련의 URL을 생성하는 것이 좋습니다. 이를 위해 Pages folder에 _Host.cshtml이름의 file을 아래와 같이 추가합니다.

@page "/"

@{
	Layout = null;
}
<!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">
		<component type="typeof(MyBlazorApp.Blazor.Routed)" render-mode="Server" />
	</div>
	<script src="_framework/blazor.server.js"></script>
</body>
</html>

예제의 Page는 이전 예제에서 정의된 Routed component를 적용하는 component요소와 Blazor JavaScript code를 위한 script요소, Bootstrap CSS stylesheet를 위한 link요소를 포함하고 있습니다. 이제 _Host.cshtml file을 현재 존재하는 URL route와 요청이 일치하지 않을 경우 대안으로 사용할 수 있도록 아래 Program.cs와 같이 설정을 추가합니다.

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

MapFallbackToPage method는 routing system이 일치하지 않는 요청을 위한 마지막 수단으로서 _Host page를 사용하도록 합니다.

 

(2) Component에 Route 추가하기

 

Component는 @page 지시자를 사용해 표시되어야할 URL을 선언합니다. 아래 예제는 Blazor folder의 ProductList.razor file을 변경한 것으로 @page지시자가 추가되었습니다.

@page "/product"

<TableTemplate RowType="Product" RowData="Products" Highlight="@(p => p.ProductManufacturer.ManufacturerInc)" SortDirection="@(p => p.ProductName)">

예제에서의 @page지시자는 ProductList component가 /product URL의 요청에서 해당 component가 표시되어야 함을 의미합니다. Component는 하나이상의 routing을 여러 @page지시자를 통해 선언할 수 있습니다. 아래 예제는 Blazor foler의 CategoryList.razor file을 변경한 것으로 2개의 URL에 대한 지시자가 추가되었습니다.

@page "/categorylist"
@page "/category"

<CascadingValue Name="BgTheme" Value="Theme" IsFixed="false" >

아래글에서 소개된 routing pattern 기능은 catchall segment 변수와 선택적 segment 변수를 제외하고 대부분 @page 표현식에서 사용될 수 있습니다.

 

[.NET/ASP.NET] - ASP.NET Core - 2. 라우팅(Routing)

 

ASP.NET Core - 2. 라우팅(Routing)

URL routing의 기본적인 기능은 요청 URL에 따라 그에 맞는 처리를 실행하여 응답을 생성하는 것입니다. 이제 예제를 통해 Routing에 관한 전반적인 내용을 살펴보도록 하겠습니다. 1. 시작하기 예제는

lab.cliel.com

segment변수가 있는 2개의 @page 표현식을 사용하면 선택적 변수기능을 재생성할 수 있는데 이와 관련된 내용은 다른 글을 통해서 알아볼 것입니다.

 

기본적인 Razor Component routing 기능이 어떻게 작동하는지를 알아보기 위해 project를 실행하고 /product와 /category URL을 요청합니다. 그러면 application에서 각각의 URL에 해당하는 component가 다음과 같이 표시될 것입니다.

● 기본 Component Route 설정하기

 

위 Program.cs에서의 설정변경은 요청에 대한 대체 route를 지정하는 것이며 application의 component 중 하나에는 application의 기본 URL인 '/'에 표시될 component를 식별하기 위해 여기에 해당하는 route가 필요합니다. 아래 예제는 Blazor folder의 ProductList.razor file을 변경한 것으로 기본 route를 정의한 것입니다.

@page "/product"
@page "/"

<TableTemplate RowType="Product" RowData="Products" Highlight="@(p => p.ProductManufacturer.ManufacturerInc)" SortDirection="@(p => p.ProductName)">

project를 실행하면 기본 / URL이 요청될 것이며 ProductList component에서 생성한 다음과 같은 응답을 볼 수 있게 됩니다.

(3) Route된 component사이 탐색

 

기본 routing 설정은 제대로 적용되어 있지만 독립적인 component에 비해 route를 사용하는 것이 어떤 이점을 제공하는지에 대해서는 분명하지 않을 수 있습니다. 이와 관련된 개선점은 routing system과 연결된 anchor요소를 render 하는 NavLink component를 통해 Blazor folder의 ProductList.razor file예제에서와 같이 확인할 수 있습니다.

</TableTemplate>

<NavLink class="btn btn-primary" href="/category">Category</NavLink>

@code {

ASP.NET Core의 다른 부분에서 사용된 anchor요소와는 달리 NavLink component는 component, page 또는 action 이름이 아닌 URL을 사용해 설정됩니다. 또한 예제에서의 NavLink 요소는 Category component의 @page 지시자에 의해 지원되는 URL로 탐색을 시도합니다.

 

탐색자체는 programming을 통한 방식으로도 수행될 수 있는데 이러한 방법은 component가 event에 대응하여 다른 URL로 이동이 필요한 경우 Blazor folder의 CategoryList.razor예제에서와 같이 유용하게 사용될 수 있습니다.

<SelectFilter Title="@("Theme")" Values="Themes" @bind-SelectedValue="Theme" />

<button class="btn btn-primary" @onclick="HandleClick">Product</button>

@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" };

	[Inject]
	public NavigationManager? NavManager { get; set; }
	public void HandleClick() => NavManager?.NavigateTo("/product");
}

NavigationManager class는 탐색을 위한 programm적인 접근을 제공하는 것으로 아래 table에서는 NavigationManager class에서 가장 중요한 member들을 소개하고 있습니다.

NavigateTo(url) 해당 method는 새로운 HTTP요청을 보내지 않으면서 특정한 URL로의 탐색을 시도합니다.
ToAbsoluteUri(path) 해당 method는 상대적 경로를 완전한 하나의 URL로 변환합니다.
ToBaseRelativePath(url) 해당 method는 완전한 URL로 부터 상대경로를 추출합니다.
LocationChanged 해당 event는 위치가 변경되는 경우 trigger됩니다.
Uri 해당 속성은 현재 URL을 반환합니다.

NavigationManager class는 service로서 제공되며 의존성 주입 기능으로 접근하기 위한 Inject attribute를 사용하는 Razor Component에 의해 수신됩니다.

 

NavigationManager.NavigateTo method는 URL을 탐색하는 것으로 위 예제에서는 ProductList component에서 처리될 /product URL로의 탐색을 위해 사용되었습니다.

 

위 구현 결과를 확인해 보기 위해 project를 실행하고 /product URL을 요청합니다. 해당 응답에서 button으로 표현된 Category link를 click하면click 하면 CategoryList component가 다음과 같이 표시될 것입니다. 여기서 다시 Product link를 click 하면 곧이어 ProductList component가 반환됨을 알 수 있습니다.

만약 위 과정을 F12 개발자 도구를 열어 수행한다면 하나의 component에 다음 component로의 변환이 browser상에 URL이 변경됨에도 불구하고 각각의 분리된 HTTP의 요청없이도 이루어진다는 것을 알 수 있습니다. Blazor는 첫 component가 표시될 때 성립된 지속적 HTTP연결과 새로운 HTML 문서의 loading 없이 탐색을 수행할 수 있는 JavaScript API의 사용을 통해 각각의 component에서 생성된 content를 전달합니다.

NavigationManager.NavigateTo method는 true일때 browser가 새로운 HTTP 요청을 보내고 HTML문서를 reload 하도록 강제하는 선택적 인수를 허용합니다.

(4) Routing Data 수신하기

 

Component는 Parameter attribute를 사용함으로서 segment 변수를 수신할 수 있습니다. 이를 확인해 보기 위해 Blazor folder에 ItemDisplay.razor이름의 file을 아래와 같이 추가합니다.

@page "/item"
@page "/item/{id:long}"

<h5>Editor for Product: @Id</h5>

<NavLink class="btn btn-primary" href="/product">Return</NavLink>

@code {
	[Parameter]
	public long Id { get; set; }
}

해당 component는 아직까지는 routing data로 부터 수신한 값을 표시하는 것 외에 어떠한 것도 하지 않습니다. 여기서 @page표현식은 long으로 지정된 id라는 이름의 segment 변수를 포함하고 있으며 component에서는 같은 이름의 속성을 정의하고 여기에 Parameter attribute를 적용함으로써 할당된 segment 변수로 값을 수신하게 됩니다.

@page표현식에서 segment 변수에 대한 type을 지정하지 않으면 string으로 속성의 type을 설정해야 합니다.

아래 예제는 Blazor folder의 ProductList.razor file을 변경한 것으로 ProductList component에 표시된 각각의 product개체를 위한 link를 생성하기 위해 NavLink component를 적용한 것입니다.

@page "/product"
@page "/"

<TableTemplate RowType="Product" RowData="Products" Highlight="@(p => p.ProductManufacturer.ManufacturerInc)" SortDirection="@(p => p.ProductName)">
	<Header>
		<tr>
			<th>ID</th>
			<th>Name (Price)</th>
			<th>Category</th>
			<th>Manufacturer</th>
			<th></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>
		<td>
			<NavLink class="btn btn-sm btn-info" href="@GetEditUrl(p.ProductId)">
				Edit
			</NavLink>
		</td>
	</RowTemplate>
</TableTemplate>

<NavLink class="btn btn-primary" href="/category">Category</NavLink>

@code {
	[Inject]
	public BlazorTDBContext? Context { get; set; }

	public IEnumerable<Product>? Products => Context?.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer);

	public string GetEditUrl(long id) => $"/item/{id}";
}

Razor component에서는 attribute값에서 정적 content와 Razor 표현식의 혼합을 지원하지 않습니다. 대신 GetEditUrl method를 정의하고 이를 통해 NavLink href 속성의 값을 생성하기 위해 호출되는 각 Product개체에 대한 탐색 URL을 생성하고 있습니다.

 

project를 실행하고 /product로 URL을 요청한 뒤 표시되는 Edit button중 하나를 click 합니다. 그러면 browser는 새로운 URL로 HTML문서를 다시 loading 하는 것 없이 탐색을 시도하게 되며 ProductDisplay component에 의해 생성된 대체 content를 다음과 같이 표시하게 됩니다. 그리고 이러한 동작을 통해 어떤 방식으로 component가 routing system으로부터 data를 수신할 수 있는지를 알 수 있을 것입니다.

(5) Layout을 사용해 공용 content정의하기

 

Layout은 Razor Component를 위해 공용 content를 제공하는 template component입니다. layout을 생성하기 위해 NavLayout.razor이름의 Razor Component를 Blazor folder에 아래와 같이 추가합니다.

@inherits LayoutComponentBase

<div class="container-fluid">
	<div class="row">
		<div class="col-3">
			<div class="d-grid gap-2">
				@foreach (string key in NavLinks.Keys)
				{
					<NavLink class="btn btn-outline-primary" href="@NavLinks[key]" ActiveClass="btn-primary text-white" Match="NavLinkMatch.Prefix">
						@key
					</NavLink>
				}
			</div>
		</div>
		<div class="col">
			@Body
		</div>
	</div>
</div>

@code {
	public Dictionary<string, string> NavLinks = new Dictionary<string, string> { {"Prodcut", "/Product" }, {"Category", "/category" }, {"Details", "/item" } };
}

Layout은 @inherits표현식을 통해 Razor Component로 부터 생성되는 class의 기반으로서 LayoutComponentBase class를 지정합니다. LayoutComponentBase class는 Body라는 이름의 RenderFragment class를 정의하며 이것은 layout으로 표시되는 공용 content안에서의 component로부터 content를 지정하는 데 사용됩니다. 예제에서 layout component는 grid layout을 생성하고 있는데 이는 application에서 각 component에 대한 일련의 NavLink component를 표시하는 것입니다. 또한 NavLink component는 아래 표에 명시된 2개의 새로운 attribute를 통해 설정됩니다.

ActiveClass 해당 attribute는 NavLink component에 의해 render되는 anchor요소가 현재 URL과 href attribute의 값이 일치할때 추가될 하나 또는 그 이상의 CSS class를 지정합니다.
Match 해당 attribute는 현재 URL과 href attribute의 값이 일치하게 되는 방식을 NavLinkMatch 열거형으로 부터의 값을 사용해 지정합니다. Prefix값은 href와 URL의 시작과 일치하는 경우이며 All은 전체 URL과 일치하는 경우를 의미합니다. 

NavLink component는 Prefix matching을 사용해 설정되었으며 일치함으로 판단되는 경우 Bootstrap btn-primary과 text-white class에 render하는 anchor요소를 추가합니다.

 

● Layout 적용

 

layout을 적용할 수 있는 방법으로는 3가지가 있습니다. 우선 component에서는 자체 layout을 @layout 표현식을 사용해 선택할 수 있습니다. 상위(부모)에서는 내장된 LayoutView component를 통해 하위 component를 wrapping 함으로써 layout을 사용할 수 있고 layout자체는 모든 component에서 RouteView component의 DefaultLayout attribute를 설정함으로써  Blazor folder의 Routed.razor file을 변경한 아래 예제와 같이 적용될 수 있습니다.

<RouteView RouteData="@context" DefaultLayout="typeof(NavLayout)" />

project를 실행하고 /product로 URL을 요청합니다. 그러면 layout은 ProductList component에서 render된 content를 표시할 것입니다. 여기서 layout의 왼쪽에 있는 탐색 button은 application전역을 탐색하는 데 사용할 수 있습니다.

project를 실행한 직후 / URL요청에 의해서도 ProductList component에서의 content를 볼 수 있습니다. 다만 Product 탐색 button은 강조되어 표시되지 않을 뿐입니다. 이 문제를 해결하는 방법은 잠시 후 알아볼 것입니다.

 

3. Component 생명주기 method

 

Razor Component는 생명주기가 명확하며 이는 component가 주요 변화에 대한 알림을 수신하기 위해 구현할 수 있는 아래 표의 method를 통해 제공됩니다.

OnInitialized()
OnInitializedAsync()
해당 method는 component가 처음 초기화될때 호출됩니다.
OnParametersSet()
OnParametersSetAsync()
해당 method는 적용된 Parameter attribute를 통해 속성의 값이 적용된 이 후 호출됩니다.
ShouldRender() 해당 method는 component의 content가 사용자에게 제공된 content를 update하기 위해 render되기 전 호출됩니다. 만약 method가 false를 반환한다면 component의 content는 render되지 않으며 update는 억제됩니다. 참고로 해당 method는 component의 초기 rendering은 억제하지 않습니다.
OnAfterRender(first)
OnAfterRenderAsync(first)
해당 method는 component의 content가 render된 이 후 호출됩니다. 여기서 bool 매개변수는 Blazor가 component에 대한 초기 render를 수행할때 true입니다.

OnInitialized 혹은 OnParameterSet method는 component의 초기상태를 설정할때 유용하게 사용될 수 있습니다. 위에서 정의된 layout에서는 NavLink component가 오로지 단일 URL로만 일치하지 때문에 기본 URL을 처리하지 않으며 이와 동일한 문제가 /category와 /categorylist 경로의 사용을 필요로 하는 CategoryList component에서도 존재합니다.

* Routed Component에 대한 생명주기
URL routing을 사용할때 component는 URL이 바뀔 때 화면에 표시되는 것으로부터 제거될 수 있습니다. Component는 System.IDisposable interface를 구현할 수 있고 Blazor는 해당 method를 component가 제거될 때 호출할 수 있습니다.

여러 URL과 일치하는 component의 생성은 생명주기 method의 사용을 필요로 합니다. 왜 그런지를 알아보기 위해 Blazor folder에 MultiNavLink.razor이름의 component를 아래와 같이 추가합니다.

<a class="@ComputedClass" @onclick="HandleClick" href="">
	@ChildContent
</a>

@code {
	[Inject]
	public NavigationManager? NavManager { get; set; }

	[Parameter]
	public IEnumerable<string> Href { get; set; } = Enumerable.Empty<string>();
	
	[Parameter]
	public string Class { get; set; } = string.Empty;
	
	[Parameter]
	public string ActiveClass { get; set; } = string.Empty;
	
	[Parameter]
	public NavLinkMatch? Match { get; set; }

	public NavLinkMatch ComputedMatch
	{
		get => Match ?? (Href.Count() == 1 ? NavLinkMatch.Prefix : NavLinkMatch.All);
	}

	[Parameter]
	public RenderFragment? ChildContent { get; set; }

	public string ComputedClass { get; set; } = string.Empty;

	public void HandleClick()
	{
		NavManager?.NavigateTo(Href.First());
	}

	private void CheckMatch(string currentUrl)
	{
		string path = NavManager!.ToBaseRelativePath(currentUrl);
		path = path.EndsWith("/") ? path.Substring(0, path.Length - 1) : path;

		bool match = Href.Any(href => ComputedMatch == NavLinkMatch.All ? path == href : path.StartsWith(href));
		ComputedClass = match ? $"{Class} {ActiveClass}" : Class;
	}

	protected override void OnParametersSet()
	{
		ComputedClass = Class;
		NavManager!.LocationChanged += (sender, arg) => CheckMatch(arg.Location);
		Href = Href.Select(h => h.StartsWith("/") ? h.Substring(1) : h);

		CheckMatch(NavManager!.Uri);
	}
}

예제의 component는 보통 NavLink요소와 같은 방식으로 작동하기는 하지만 일치할 수 있는 여러 경로의 배열을 취급할 수 있습니다. 또한 component는 각각의 경로를 추출하는 것과 같은 Parameter attribute가 적용된 속성에 값이 할당될때까지 수행될 수 없는 몇몇 초기 설정이 필요하기 때문에 OnParametersSet 생명주기 method에 의존하게 됩니다.

 

해당 component는 NavigationManager class에 정의된 LocationChanged event를 청취함으로서 현재 URL에 대한 변화에 대응합니다. 이때 event의 Location속성은 현재 URL을 통해 component를 제공하고 있으며 anchor요소에 대한 CSS class를 변경하는 데 사용됩니다. 아래는 Blazor folder의 NavLayout.razor file을 변경한 것으로 위의 새로운 component를 적용하였습니다.

@inherits LayoutComponentBase

<div class="container-fluid">
	<div class="row">
		<div class="col-3">
			<div class="d-grid gap-2">
				@foreach (string key in NavLinks.Keys)
				{
					<MultiNavLink class="btn btn-outline-primary btn-block" href="@NavLinks[key]" ActiveClass="btn-primary text-white">
						@key
					</MultiNavLink>
				}
			</div>
		</div>
		<div class="col">
			@Body
		</div>
	</div>
</div>

@code {
	public Dictionary<string, string[]> NavLinks = new Dictionary<string, string[]> { { "Prodcut", new string[] { "/Product", "/" } }, {"Category", new string[] { "/category", "/categorylist" } }, {"Details", new string[] { "/item" } } };
}

위 예제에서는 기존에 비해서 Match attribute가 제거되었습니다. 새로운 component에서도 해당 attribute를 지원하기는 하지만 기본적으로는 href attribute를 통해 수신하는 경로의 수에 따라 일치합니다.

 

project를 실행하고 /와 /categorylist로의 URL을 요청합니다. 두개의 URL모두 인식하게 되며 해당 탐색 button이 같이 강조되어 아래와 같이 표시될 것입니다.

(1) 비동기 Task를 위한 생명주기 method사용

 

생명주기 method는 component로 부터의 초기 content가 render 된 이후 완료되어야 할 작업(database로의 질의등)을 수행하는 데에도 사용할 수 있습니다. 아래 예제는 ItemDisplay component에서 placeholder content를 대체하여 생명주기 method를 매개변수로 수신된 값을 사용하여 database로의 질의에 사용하도록 변경한 것입니다.

@page "/item"
@page "/item/{id:long}"

@if (Item == null)
{
	<h5 class="bg-info text-white text-center p-2">Loading...</h5>
}
else
{
	<table class="table table-striped table-bordered">
		<tbody>
			<tr><th>Id</th><td>@Item.ProductId</td></tr>
			<tr><th>Name</th><td>@Item.ProductName</td></tr>
			<tr><th>Price</th><td>@Item.ProductPrice</td></tr>
		</tbody>
	</table>
}
<button class="btn btn-outline-primary" @onclick="@(() => HandleClick(false))">
	Previous
</button>
<button class="btn btn-outline-primary" @onclick="@(() => HandleClick(true))">
	Next
</button>

@code {
	[Inject]
	public BlazorTDBContext? Context { get; set; }

	[Inject]
	public NavigationManager? NavManager { get; set; }

	[Parameter]
	public long Id { get; set; } = 0;

	public Product? Item { get; set; }

	protected async override Task OnParametersSetAsync()
	{
		await Task.Delay(1000);

		if (Context != null)
		{
			Item = await Context.Product.FirstOrDefaultAsync(p => p.ProductId == Id) ?? new Product();
		}
	}

	public void HandleClick(bool increment)
	{
		Item = null;
		NavManager?.NavigateTo($"/item/{(increment ? Id + 1 : Id - 1)}");
	}
}

매개변수의 값이 설정될때 까지 component는 database에 질의를 수행할 수 없으므로 Item속성의 값은 OnParametersSetAsync method안에서 가져오게 됩니다. database는 ASP.NET Core server와 함께 동작중이므로 database의 질의를 수행하기 전 약간의 delay를 추가함으로써 component가 작동방식을 알아볼 수 있도록 하였습니다.

 

item속성의 값은 질의가 완료될때 까지는 null이 되는데 이때 질의의 결과에 따른 개체가 되거나 질의의 결과로 아무것도 존재하는 않는다면 새로운 Product개체가 값이 될 것입니다. 참고로 loading message는 Item개체가 null인 동안에만 표시됩니다.

 

project를 실행하고 table에 표시된 button중 하나를 click 합니다. 그러면 ItemDisplay component가 해당 data의 요약화면을 다음과 같이 표시하게 될 것입니다.

이 상태에서 Previous나 Next button을 click하면 인접한 primary key값을 통해 개체의 질의를 수행하게 되고 다음과 같은 응답을 생성하게 됩니다.

Blazor는 사용자에게 content를 표시하기 전까지는 OnParametersSetAsync method에서 수행되는 Task를 완료할 때까지 대기하지 않음에 주목해야 합니다. 이것은 Item속성이 null일 때 loading message가 표시되도록 하는 것입니다. 일단 Task가 완료되고 Item속성으로 값이 할당되면 component의 view는 자동적으로 다시 render 되고 변경사항이 지속적 HTTP연결을 통해 browser로 전송되어 사용자에게 표시됩니다.

 

4. Component 상호작용 관리

 

대부분의 component는 매개변수, event를 통해 application과 사용자와의 상호작용이 가능이 가능하도록 합니다. 여기서 Blazor는 component와의 상호작용을 관리하기 위한 개선된 option들을 제공하고 있으며 이와 관련하여 필요한 부분을 알아볼 것입니다.

 

(1) 하위 Component 참조

 

상위 component에서는 하위 component에 대한 참조를 가져올 수 있고 이를 통해 정의된 속성과 method를 사용할 수 있습니다. 아래 예제는 Blazor folder의 MultiNavLink.razor file을 변경한 것으로 disable 상태를 추가한 것입니다.

<a class="@ComputedClass" @onclick="HandleClick" href="">
	@if (Enabled)
	{
		@ChildContent
	}
	else
	{
		@("Disabled")
	}
</a>

@code {
	[Inject]
	public NavigationManager? NavManager { get; set; }

	[Parameter]
	public IEnumerable<string> Href { get; set; } = Enumerable.Empty<string>();
	
	[Parameter]
	public string Class { get; set; } = string.Empty;
	
	[Parameter]
	public string ActiveClass { get; set; } = string.Empty;

	[Parameter]
	public string DisabledClasses { get; set; } = string.Empty;
	
	[Parameter]
	public NavLinkMatch? Match { get; set; }

	public NavLinkMatch ComputedMatch
	{
		get => Match ?? (Href.Count() == 1 ? NavLinkMatch.Prefix : NavLinkMatch.All);
	}

	[Parameter]
	public RenderFragment? ChildContent { get; set; }

	public string ComputedClass { get; set; } = string.Empty;

	public void HandleClick()
	{
		NavManager?.NavigateTo(Href.First());
	}

	private void CheckMatch(string currentUrl)
	{
		string path = NavManager!.ToBaseRelativePath(currentUrl);
		path = path.EndsWith("/") ? path.Substring(0, path.Length - 1) : path;

		bool match = Href.Any(href => ComputedMatch == NavLinkMatch.All ? path == href : path.StartsWith(href));

		if (!Enabled)
		{
			ComputedClass = DisabledClasses;
		}
		else
		{
			ComputedClass = match ? $"{Class} {ActiveClass}" : Class;
		}
	}

	protected override void OnParametersSet()
	{
		ComputedClass = Class;
		NavManager!.LocationChanged += (sender, arg) => CheckMatch(arg.Location);
		Href = Href.Select(h => h.StartsWith("/") ? h.Substring(1) : h);

		CheckMatch(NavManager!.Uri);
	}

	private bool Enabled { get; set; } = true;

	public void SetEnabled(bool enabled)
	{
		Enabled = enabled;
		CheckMatch(NavManager!.Uri);
	}
}

아래 예제는 Blazor folder의 NavLayout.razor file을 변경한 것으로 공유된 layout에서 MultiNavLink component에 대한 참조와와 Enabled 속성의 값을 전환하는 button을 유지하도록 하고 있습니다.

@inherits LayoutComponentBase

<div class="container-fluid">
	<div class="row">
		<div class="col-3">
			<div class="d-grid gap-2">
				@foreach (string key in NavLinks.Keys)
				{
					<MultiNavLink class="btn btn-outline-primary btn-block" href="@NavLinks[key]" ActiveClass="btn-primary text-white" DisabledClasses="btn btn-dark text-light btn-block disabled" @ref="Refs[key]">
						@key
					</MultiNavLink>
				}
				<button class="btn btn-secondary btn-block mt-5" @onclick="ToggleLinks">
					Toggle Links
				</button>
			</div>
		</div>
		<div class="col">
			@Body
		</div>
	</div>
</div>

@code {
	public Dictionary<string, string[]> NavLinks = new Dictionary<string, string[]> { { "Prodcut", new string[] { "/Product", "/" } }, {"Category", new string[] { "/category", "/categorylist" } }, {"Details", new string[] { "/item" } } };

	public Dictionary<string, MultiNavLink> Refs = new Dictionary<string, MultiNavLink>();

	private bool LinksEnabled = true;

	public void ToggleLinks()
	{
		LinksEnabled = !LinksEnabled;

		foreach (MultiNavLink link in Refs.Values)
		{
			link.SetEnabled(LinksEnabled);
		}
	}
}

component에 대한 참조는 @ref attribute를 추가하고 component를 할당할 field명이나 속성을 지정함으로서 생성됩니다. MultiNavLink component는 Dictionary로 순환하는 @foreach문안에서 생성되었기 때문에 참조를 유지하기 위한 간단한 방법은 아래와 같이 Dictionary에도 있습니다.

<MultiNavLink class="btn btn-outline-primary btn-block" href="@NavLinks[key]" ActiveClass="btn-primary text-white" DisabledClasses="btn btn-dark text-light btn-block disabled" @ref="Refs[key]">

각 MultiNavLink component가 생성됨에 따라 이는 Refs dictionary에도 추가되었습니다. Razor component는 표준 C# class로 compile 되며 이것으로 MultiNavLink component의 collection은 MultiNavlink개체의 collection이 됩니다.

public Dictionary<string, MultiNavLink> Refs = new Dictionary<string, MultiNavLink>();

project를 실행하고 Toggle Links button을 click하면 event handler는 ToggleLinks method를 호출하게 되고 MultiNavLink component에 대한 각 Enabled속성의 값이 다음과 같이 설정됩니다.

참조는 component의 content가 render되고 OnAfterRender/OnAfterRenderAsync lifecycle method가 호출된 이후에만 사용할 수 있습니다. 이것은 event handler에서 참조를 사용하기에 이상적이지만 이전 lifecycle method에는 적합하지 않습니다.

(2) 다른 code의 Component와 상호작용

 

Component는 ASP.NET Core application의 다른 code에서도 사용될 수 있으며 이로서 복잡한 project의 부분 간에 풍부한 상호작용이 가능합니다. 아래 예제는 Blazor folder의 MultiNavLink.razor file을 변경한 것으로 component안에 method에서 ASP.NET Core application의 다른 부분을 호출할 수 있도록 함으로써 탐색 button을 활성화하거나 비활성화하도록 하였습니다.

public void SetEnabled(bool enabled)
{
	InvokeAsync(() =>
	{
		Enabled = enabled;
		CheckMatch(NavManager!.Uri);
		StateHasChanged();
	});
}

Razor Component는 아래 표에 안내된것 처럼 Blazor환경의 외부에서 호출되는 code에 사용될 수 있는 2개의 method를 제공하고 있습니다.

InvokeAsync(func) 해당 method는 Blazor 환경내부 함수를 실행하는데 사용됩니다.
StateHasChanged() 해당 method는 정상 생명주기의 외부에서 변경사항이 발생할 때 호출됩니다.

InvokeAsync method는 Blazor환경 내부의 함수를 호출하여 변경사항이 정확히 처리될 수 있도록 합니다. StateHasChanged method는 모든 변경사항이 적용될 때 호출되며 Blazor update를 발동하고 변경사항이 component의 출력에 반영되도록 합니다.

 

application전역에서 가능한 service를 생성하기 위해 project에 Services라는 folder를 생성하고 ToggleService.cs이름의 file을 아래와 같이 추가합니다.

using MyBlazorApp.Blazor;

namespace MyBlazorApp.Services
{
	public class ToggleService
	{
		private List<MultiNavLink> components = new List<MultiNavLink>();
		private bool enabled = true;

		public void EnrolComponents(IEnumerable<MultiNavLink> comps)
		{
			components.AddRange(comps);
		}

		public bool ToggleComponents()
		{
			enabled = !enabled;
			components.ForEach(c => c.SetEnabled(enabled));
			return enabled;
		}
	}
}

해당 service는 component의 collection을 관리하고 ToggleComponents method가 호출될때 해당 모든 component들의 SetEnabled method를 호출하게 됩니다. service에서는 Blazor를 지정하는 어떤 것도 존재하지 않으며 Razor Component file이 compile 될 때 생성되는 C# class에만 의존할 뿐입니다. 아래 예제에서는 Program.cs file을 변경하여 ToggleService class를 singleton service로서 설정하도록 하였습니다.

builder.Services.AddSingleton<MyBlazorApp.Services.ToggleService>();

var app = builder.Build();

그리고 Blazor foler의 NavLayout.razor file을 변경한 아래 예제와 같이 Blazor layout을 update하여 the MultiNavLink component로의 참조가 유지되고 새로운 service로서 등록될 수 있도록 하였습니다.

@inherits LayoutComponentBase
@using MyBlazorApp.Services

<div class="container-fluid">
	<div class="row">
		<div class="col-3">
			<div class="d-grid gap-2">
				@foreach (string key in NavLinks.Keys)
				{
					<MultiNavLink class="btn btn-outline-primary btn-block" href="@NavLinks[key]" ActiveClass="btn-primary text-white" DisabledClasses="btn btn-dark text-light btn-block disabled" @ref="Refs[key]">
						@key
					</MultiNavLink>
				}
				<button class="btn btn-secondary btn-block mt-5" @onclick="ToggleLinks">
					Toggle Links
				</button>
			</div>
		</div>
		<div class="col">
			@Body
		</div>
	</div>
</div>

@code {
	[Inject]
	public ToggleService? Toggler { get; set; }

	public Dictionary<string, string[]> NavLinks = new Dictionary<string, string[]> { { "Prodcut", new string[] { "/Product", "/" } }, {"Category", new string[] { "/category", "/categorylist" } }, {"Details", new string[] { "/item" } } };

	public Dictionary<string, MultiNavLink> Refs = new Dictionary<string, MultiNavLink>();

	//private bool LinksEnabled = true;

	protected override void OnAfterRender(bool firstRender)
	{
		if (firstRender && Toggler != null)
		{
			Toggler.EnrolComponents(Refs.Values);
		}
	}

	public void ToggleLinks()
	{
		Toggler?.ToggleComponents();
	}
}

이전에 언급한 바와 같이 component 참조는 content가 render된 이후 까지는 사용할 수 없습니다. 따라서 위 예제에서는 OnAfterRender 생명주기 method를 사용하여 component 참조를 의존성 주입을 통해 수신된 service를 통해 등록하고 있습니다.

 

이제 마지막 절차로 ASP.NET Core application의 다른 부분에서 service를 사용할 수 있도록 하는 것입니다. 아래 예제에서는 Controllers folder의 HomeController.cs file을 변경하여 Home controller에서 매 요청을 처리할 때마다 ToggleService.ToggleComponents method를 호출하는 간단한 action을 추가하였습니다.

public class HomeController : Controller
{
    private BlazorTDBContext context;
	private ToggleService toggleService;

	public HomeController(BlazorTDBContext dbContext, ToggleService ts)
    {
        context = dbContext;
		toggleService = ts;
	}
    public IActionResult Index([FromQuery] string selectedManufacturer)
    {
        return View(new ProductListViewModel
        {
            Product = context.Product.Include(p => p.ProductCategory).Include(p => p.ProductManufacturer),
            Manufacturer = context.Manufacturer.Select(m => m.ManufacturerName).Distinct(),
            SelectedManufacturer = selectedManufacturer
        });
    }

	public string Toggle() => $"Enabled: {toggleService.ToggleComponents()}";
}

project를 실행하고 새로운 browser를 열어 /controllers/home/toggle URL을 요청합니다. ASP.NET Core application에 의해 두번째 요청이 처리될 때 action method는 service를 사용하여 탐색 button의 상태를 전환하게 됩니다.  따라서 /controllers/home/toggle URL을 요청할 때마다 다음과 같이 탐색 button의 상태가 바뀔 것입니다.

 

(3) JavaScript를 사용한 Component와의 상호 작용

 

Blazor는 JavaScript와 server측 C#사이의 상호작용을 위한 다양한 도구를 제공하고 있습니다.

 

● Component에서 JavaScript함수 호출

 

예제를 만들어 보기 위해 wwwroot folder에서 interop.js이름의 file을 아래와 같이 추가합니다.

var addTableRows = function(colCount) {
    let elem = document.querySelector('tbody');
    let row = document.createElement('tr');

    elem.append(row);

    for (let i = 0; i < colCount; i++) {
        let cell = document.createElement('td');
        cell.innerText = 'New Elements';

        row.append(cell);
    }
}

예제의 JavaScript code는 browser에서 제공하는 API를 사용하여 tbody요소를 찾아 table의 body를 표시하고 함수의 매개변수로 지정한 cell의 수를 포함하는 새로운 row를 추가합니다.

 

application에 해당 JavaScript file을 추가하기 위해 Blazor application을 browser로 보내는 대체 page로서 설정된 Pages folder의 _Host.cshtml Razor file예제와 같이 관련 요소를 추가합니다.

<head>
	<title>@ViewBag.Title</title>
	<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
	<script src="~/interop.js"></script>
	<base href="~/" />
</head>

아래 예제는 Blazor folder의 ItemDisplay component를 개선한 것으로 onclick event가 발생하면 JavaScript 함수를 호출하도록 되어 있는 button을 render 하도록 하였습니다. 또한 component의 생명주기 method를 설명하기 위해 사용한 delay는 주석처리하였습니다.

@if (Item == null)
{
	<h5 class="bg-info text-white text-center p-2">Loading...</h5>
}
else
{
	<table class="table table-striped table-bordered">
		<tbody>
			<tr><th>Id</th><td>@Item.ProductId</td></tr>
			<tr><th>Name</th><td>@Item.ProductName</td></tr>
			<tr><th>Price</th><td>@Item.ProductPrice</td></tr>
		</tbody>
	</table>
}

<button class="btn btn-outline-primary" @onclick="@HandleClick">
	Invoke JS Function
</button>

@code {
	[Inject]
	public BlazorTDBContext? Context { get; set; }

	[Inject]
	public NavigationManager? NavManager { get; set; }

	[Inject]
	public IJSRuntime? JSRuntime { get; set; }

	[Parameter]
	public long Id { get; set; } = 0;

	public Product? Item { get; set; }

	protected async override Task OnParametersSetAsync()
	{
		//await Task.Delay(1000);

		if (Context != null)
		{
			Item = await Context.Product.FirstOrDefaultAsync(p => p.ProductId == Id) ?? new Product();
		}
	}

	public void HandleClick(bool increment)
	{
		Item = null;
		NavManager?.NavigateTo($"/item/{(increment ? Id + 1 : Id - 1)}");
	}

	public async Task HandleClick()
	{
		await JSRuntime!.InvokeVoidAsync("addTableRows", 2);
	}
}

예제에서는 component가 의존성주입을 통해 수신하는 IJSRuntime interface를 통해 JavaScript 함수를 호출하고 있습니다. service는 Blazor 구성의 일부로서 자동으로 생성되며 아래 표의 method를 제공합니다.

InvokeAsync<T>(name, args) 해당 method는 인수로 제공된 특정한 함수를 호출하며 이에 대한 결과는 generic type 매개변수로 지정됩니다.
InvokeAsync(name, args) 해당 method는 응답을 생성하지 않는 함수를 호출하는데 사용됩니다.

위 예제에서는 InvokeVoidAsync method를 사용하여 addTableRows JavaScript 함수를 호출하도록 한 것으로 함수의 매개변수를 위한 값을 제공하고 있습니다. project를 실행하고 /item/1 URL을 호출한 뒤 'Invoke JS Function' button을 click 합니다. Blazor는 JavaScript함수를 호출하여 다음과 같이 table의 끝에 해당 row를 추가하게 될 것입니다.

 

HTML 요소에 대한 참조 유지

 

Razor Component는 생성한 HTML요소에 대한 참조를 유지하고 해당 참조를 JavaScript code로 전달할 수 있습니다. 아래 예제는 이전 예제에서의 JavaScript 함수를 변경한 것으로 매개변수를 통해 수신된 HTML요소하에서 동작할 수 있습니다.

var addTableRows = function(colCount, elem) {
    //let elem = document.querySelector('tbody');
    let row = document.createElement('tr');

    elem.parentNode.insertBefore(row, elem);

    for (let i = 0; i < colCount; i++) {
        let cell = document.createElement('td');
        cell.innerText = 'New Elements';

        row.append(cell);
    }
}

아래 예제는 Blazor folder의 ItemDisplay.razor file을 변경한 것으로 생성한 HTML요소중 하나에 대한 참조를 유지하고 이를 JavaScript함수의 인수로서 전달하고 있습니다.

@if (Item == null)
{
	<h5 class="bg-info text-white text-center p-2">Loading...</h5>
}
else
{
	<table class="table table-striped table-bordered">
		<tbody>
			<tr><th>Id</th><td>@Item.ProductId</td></tr>
			<tr @ref="RowReference"><th>Name</th><td>@Item.ProductName</td></tr>
			<tr><th>Price</th><td>@Item.ProductPrice</td></tr>
		</tbody>
	</table>
}

<button class="btn btn-outline-primary" @onclick="@HandleClick">
	Invoke JS Function
</button>

@code {
	[Inject]
	public BlazorTDBContext? Context { get; set; }

	[Inject]
	public NavigationManager? NavManager { get; set; }

	[Inject]
	public IJSRuntime? JSRuntime { get; set; }

	[Parameter]
	public long Id { get; set; } = 0;

	public Product? Item { get; set; }

	protected async override Task OnParametersSetAsync()
	{
		//await Task.Delay(1000);

		if (Context != null)
		{
			Item = await Context.Product.FirstOrDefaultAsync(p => p.ProductId == Id) ?? new Product();
		}
	}

	public void HandleClick(bool increment)
	{
		Item = null;
		NavManager?.NavigateTo($"/item/{(increment ? Id + 1 : Id - 1)}");
	}

	public ElementReference RowReference { get; set; }

	public async Task HandleClick()
	{
		await JSRuntime!.InvokeVoidAsync("addTableRows", 2, RowReference);
	}
}

@ref attribute는 HTML element를 type이 ElementReference인 속성에 할당하고 있습니다. project를 실행하고 /item/1 URL을 요청한 뒤 Invoke JS Function button을 click 합니다. ElementReference속성의 값은 InvokeVoidAsync method를 통해 JavaScript함수의 인수로서 전달되고 다음과 같은 응답을 생성하게 됩니다.

일반 HTML요소의 참조에 대한 유일한 용도는 JavaScript 함수로 전달하는 것입니다. component에서 render된 요소와의 상호작용을 위해서는 이전에 설명된 binding과 evnet기능을 사용해야 합니다.

● JavaScript에서 Component method 호출

 

JavaScript에서 C# method를 호출하는 기본적인 접근법은 static method를 사용하는 것입니다. 아래 예제는 Blazor folder의 MultiNavLink.razor file을 변경한 것으로 MultiNavLink component에 활성화된 상태를 변경하는 static method를 추가하였습니다.

public void SetEnabled(bool enabled)
{
	InvokeAsync(() =>
	{
		Enabled = enabled;
		CheckMatch(NavManager!.Uri);
		StateHasChanged();
	});
}

[JSInvokable]
public static void ToggleEnabled() => ToggleEvent?.Invoke(null, new EventArgs());

private static event EventHandler? ToggleEvent;

protected override void OnInitialized()
{
	ToggleEvent += (sender, args) => SetEnabled(!Enabled);
}

Static method는 JavaScript code에서 호출되는 것으로 JSInvokable attribute가 적용되었습니다. static method의 가장 큰 한계점은 각각의 component에 대한 update를 어렵게 만든다는 것입니다. 따라서 component의 각 instance를 처리할 static event를 정의하였습니다. event의 이름은 ToggleEvent이며 JavaScript에서 호출될 static method에 의해 발동됩니다. 또한 event를 청취하기 위해 OnInitialized 생명주기 event를 사용하였습니다. evnet가 수신되면 component의 활성상태는 변경사항이 Blazor외부에서 만들어질 때 필요한 InvokeAsync와 StateHasChanged method를 사용하는 instance method인 SetEnabled을 통해 전환됩니다.

 

아래 예제는 wwwroot folder의 interop.js File을 변경한 것으로 click시 static c# method를 호출하는 button요소를 생성하기 위한 함수를 추가하였습니다.

var createToggleButton = function() {
    let sibling = document.querySelector('button:last-of-type');
    let button = document.createElement('button');

    button.classList.add('btn', 'btn-secondary', 'btn-block');
    button.innerText = 'JS Toggle';

    sibling.parentNode.insertBefore(button, sibling.nextSibling);

    button.onclick = () => DotNet.invokeMethodAsync('MyBlazorApp', 'ToggleEnabled');
}

위 예제 함수는 현재 존재하는 button요수중 하나를 찾아 그다음 순서에 새로운 button을 추가하도록 하는 것입니다. 여기서 button이 click 되면 아래와 같이 component의 method가 호출되는 것입니다.

button.onclick = () => DotNet.invokeMethodAsync('MyBlazorApp', 'ToggleEnabled');

여기서는 C# 메소드에 사용되는 JavaScript 함수의 대문자 사용에 주의를 기울이는 것이 중요합니다. 예제에서는 DotNet다음에 마침가 오고 invokeMethodAsync method가 오는데 소문자 i로 시작하고 있습니다. 인수로는 assembly의 이름이 되며 그다음 정적 method의 이름이 올 수 있습니다.(component의 이름은 필요하지 않습니다.)

 

위 예제에서 함수가 찾는 button요소는 Blazor가 content를 사용자에게 render할때까지는 사용할 수 없습니다. 이러한 이유로 Blazor folder의 NavLayout.razor file을 변경한 아래 예제와 같 NavLayout component에 정의된 OnAfterRenderAsync method에 content의 render가 완료될 때 JavaScript 함수를 호출하는 구문을 추가하였습니다.(NavLayout component는 static method가 호출될 때 영향을 받는 MultiNavLink component의 상위이며 JavaScript함수가 한 번만 호출되도록 합니다.)

@code {
	[Inject]
	public ToggleService? Toggler { get; set; }

	[Inject]
	public IJSRuntime? JSRuntime { get; set; }

	public Dictionary<string, string[]> NavLinks = new Dictionary<string, string[]> { { "Prodcut", new string[] { "/Product", "/" } }, {"Category", new string[] { "/category", "/categorylist" } }, {"Details", new string[] { "/item" } } };

	public Dictionary<string, MultiNavLink> Refs = new Dictionary<string, MultiNavLink>();

	//private bool LinksEnabled = true;

	protected async override Task OnAfterRenderAsync(bool firstRender)
	{
		if (firstRender && Toggler != null)
		{
			Toggler.EnrolComponents(Refs.Values);
			await JSRuntime!.InvokeVoidAsync("createToggleButton");
		}
	}

	public void ToggleLinks()
	{
		Toggler?.ToggleComponents();
	}
}

project를 실행합니다. Blazor가 content를 render하고 나면 JavaScript 함수가 호출되어 2개의 button을 생성하게 됩니다. button을 click 하면 static method가 호출되고 탐색 button의 상태를 전환하는 event가 발동되어 Blazor의 상태가 다음과 같이 바뀌게 됩니다.

● JavaScript 함수에서 Instance Method의 호출

 

이전 예제의 복잡한 부분은 Razor Component 개체를 update하기 위한 static method의 응답으로 인해 비롯됩니다. 하지만 instance method 참조를 통해 JavaScript code를 제공함으로써 직접적으로 호출이 가능하도록 할 수 있습니다.

 

이에 대한 첫번째 절차로 JSInvokable attribute를 JavaScript code가 호출할 method에 추가하는 것입니다. 아래 예제는 Services folder의 ToggleService.cs file을 변경한 것으로 attribute를 적용해 ToggleService class에서 정의된 ToggleComponents method를 호출할 수 있도록 하고 있습니다.

[JSInvokable]
public bool ToggleComponents()
{
	enabled = !enabled;
	components.ForEach(c => c.SetEnabled(enabled));
	return enabled;
}

그다음으로 Blazor folder의 NavLayout.razor file에서와 같이 호출될 method에 대한 개체의 참조를 통해 JavaScript 함수를 제공하는 것입니다.

protected async override Task OnAfterRenderAsync(bool firstRender)
{
	if (firstRender && Toggler != null)
	{
		Toggler.EnrolComponents(Refs.Values);
		await JSRuntime!.InvokeVoidAsync("createToggleButton", DotNetObjectReference.Create(Toggler));
	}
}

예제의 DotNetObjectReference.Create method는 JSRuntime.InvokeVoidAsync method를 사용하는 인수로서 JavaScript함수로 전달되는 개체에 대한 참조를 생성합니다. 마지막으로 wwwroot folder의 interop.js File에서와 같이 JavaScript에서 개체잠조를 수신하고 button요소가 click 될 때 method를 호출하도록 합니다.

var createToggleButton = function (toggleServiceRef) {
    let sibling = document.querySelector('button:last-of-type');
    let button = document.createElement('button');

    button.classList.add('btn', 'btn-secondary', 'btn-block');
    button.innerText = 'JS Toggle';

    sibling.parentNode.insertBefore(button, sibling.nextSibling);

    button.onclick = () => toggleServiceRef.invokeMethodAsync('ToggleComponents');
}

예제의 JavaScript함수는 C# 개체에 대한 참조를 매개변수로 수신하고 invokeMethodAsync를 사용해 인수로 method이 이름을 지정하여 호출합니다.(필요한 경우 method에 대한 인수 또한 제공될 수 있습니다.)

 

project를 실행하고 'JS Toggle button'을 click합니다. 그러면 다음과 같이 이전과 동일한 응답을 생성할 것입니다. 하지만 component에 대한 변경사항은 ToggleService개체를 통해 관리됩니다.

 

728x90