ASP.NET Core - 13. View Component
View Component는 Partial View를 지원하기 위한 action-style logic을 제공하는 class로서 View Component가 View에 내장될 복잡한 content를 제공하는 동시에 이를 지원하는 C# code를 쉽게 유지관리할 수 있도록 지원합니다.
1. Project 준비하기
이전에 사용하던 Project를 예제로 계속사용할 것입니다. 다만 Project의 Models folder에 city.cs라는 이름의 file을 아래 내용으로 추가하고
namespace MyWebApp.Models
{
public class City
{
public string? Name { get; set; }
public string? Country { get; set; }
public int? Population { get; set; }
}
}
CitiesData.cs이름의 file도 아래와 같이 추가합니다.
namespace MyWebApp.Models
{
public class CitiesData
{
private List<City> cities = new List<City> {
new City { Name = "London", Country = "UK", Population = 8539000},
new City { Name = "New York", Country = "USA", Population = 8406000 },
new City { Name = "San Jose", Country = "USA", Population = 998537 },
new City { Name = "Paris", Country = "France", Population = 2244000 }
};
public IEnumerable<City> Cities => cities;
public void AddCity(City newCity)
{
cities.Add(newCity);
}
}
}
CitiesData clsss는 City개체의 collection에 대한 접근을 제공하며 AddCity method를 통해 collection에 새로운 개체를 추가할 수 있도록 지원하고 있습니다. 이제 Program.cs를 아래와 같이 변경하여 CitiesData class에 대한 service를 생성합니다.
builder.Services.AddSingleton<CitiesData>();
var app = builder.Build();
예제에서 추가된 새로운 구문은 AddSingleton method를 통하여 CitiesData service를 생성하고 있습니다. 이때 해당 service에 대한 interface나 구현 구분이 없는데 이것은 CitiesData개체를 배포하기 위해 service를 생성했기 때문입니다. 마지막으로 Project의 Pages folder에 Cities.cshtml이름의 Razor Page를 아래와 같이 추가합니다.
@page
@inject CitiesData Data
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
@foreach (City c in Data.Cities)
{
<tr>
<td>@c.Name</td>
<td>@c.Country</td>
<td>@c.Population</td>
</tr>
}
</tbody>
</table>
</div>
Project를 실행하고 /cities로 URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다.
2. View Component
Application은 일반적으로 View안에 Application의 주요 목적과는 관련이 없는 content를 포함할 수 있습니다. 일반적인 예로 site의 탐색도구나 별도의 page를 방문하지 않아도 사용자가 곧장 Log in할 수 있는 panel과 같은 것을 들 수 있습니다.
이러한 기능의 유형에 대한 data는 action method나 page model로 부터 view로 전달된 model data에는 해당되지 않습니다. 때문에 예제 Project의 data에 대해서는 2개의 source를 생성하였는데 이것으로 Entity Framework Core repository와 여기에 포함되는 Product, Category, Supplier 개체로 부터 data를 수신하는 View에서 City data를 사용해 생성된 몇몇 content를 표시하도록 하였습니다.
Partial view는 View에서 필요로 하며 재사용가능한 markup을 생성하는데 사용됨으로써 Application의 여러 곳에서 content를 중복시켜야 하는 필요성을 제거할 수 있습니다. Partial view는 분명 유용한 기능이긴 하지만 단지 HTML과 지시자의 조각만을 포함할 수 있으며 동작에 필요한 data는 부모 view로부터 수신받습니다. 때문에 만약 다른 data를 표현해야 한다면 약간의 문제가 생길 수 있는데, partial view로부터 직접적으로 필요한 data에 접근할 수 있기는 하지만 일반적인 개발 model을 벗어나야 하며 이해하기 어렵고 유지관리가 힘든 Application을 만들어낼 수도 있습니다. 대신 Application에서 사용되는 View model을 확장하여 필요한 data를 포함시킬 수 있으나 이런 경우에도 많은 Action method를 변경해야 할 수 있고 곧 효율적인 관리와 testing이 가능한 action method의 기능적인 분리가 어려워질 수 있습니다.
바로 이러한 문제들이 View component가 필요한 이유가 될 수 있습니다. View component는 필요한 data와 함께 partial view를 제공하며 action method 혹은 Razor Page로 부터 독립적인 C# class에 해당합니다. 이와 관련해 View component는 특별한 action 혹은 page로 생각될 수 있지만 data와 함께 partial view를 제공하는데만 사용되는 것이며 HTTP요청을 받을 수 없고 제공하는 content는 항상 부모 view안으로 포함되어야 합니다.
3. View Component의 생성과 사용
View component는 이름의 끝이 ViewComponent로 끝나는 class이면서 Invoke 또는 InvokeAsync method를 정의하거나 ViewComponent base class로 부터 파생되거나 또는 ViewComponent attribute로 적용된 class를 의미합니다.
View component는 project의 어디서든 정의될 수 있지만 대게는 Components라는 이름의 folder에서 이들을 정의하여 group화 하는 것이 관례입니다. Project에 Components foder를 추가하고 CitySummary.cs이름의 file을 해당 folder에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Components
{
public class CitySummary : ViewComponent
{
private CitiesData data;
public CitySummary(CitiesData cdata)
{
data = cdata;
}
public string Invoke()
{
return $"{data.Cities.Count()}개의 도시에 {data.Cities.Sum(c => c.Population)}명의 사람들.";
}
}
}
View component에서도 역시 필요한 service를 사용하기 위한 의존성 주입의 이점을 활용할 수 있습니다. 예제에서 view component는 CitiesData class의 의존성을 선언하고 있고 이를 통해 Invoke method에서 총도시의 수와 전체 인구의 수를 나타내는 문자열을 생성하는 데 사용되었습니다.
(1) View component 적용
View component는 2가 방법을 통해 적용할 수 있습니다. 그 중 첫 번째는 View와 Razor Page로부터 생성된 C# class에 component속성을 사용하는 것입니다. 이 속성은 IViewComponentHelper interface를 구현한 개체를 반환하며 InvokeAsync method를 제공하게 됩니다. 아래 예제는 이러한 방식을 통하여 VIews/Home에 있는 Index.cshtml file에서 View component를 적용한 결과를 나타내고 있습니다.
@section Footer {
평균 단가의 @(((Model?.UnitPrice / ViewBag.AveragePrice) * 100).ToString("F2")) %
}
@section Summary {
<div class="bg-info text-white m-2 p-2">
@await Component.InvokeAsync("CitySummary")
</div>
}
View component는 Component.InvokeAsync method를 통해 적용되었으며 이때 인수로 view component의 이름을 전달하고 있습니다. View component class는 Invoke와 InvokeAsync 둘 다 선언할 수 있는데 이것은 그저 동기화로 수행할 것인가 비동기로 수행할 것인가를 결정하기만 합니다. 하지만 Component.InvokeAsync method는 항상 사용되어야 하는데 심지어 View component가 Invoke method를 선언하고 전체적으로 동기화로 사용된다고 하더라도 이러한 규칙은 동일하게 유지됩니다.
View component를 위한 namespace를 View를 포함하는 list에 추가하기 위해 Views folder에 있는 _ViewImports.cshtml file에 아래 예제와 같은 구문을 추가합니다.
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using MyWebApp.Components
Project를 실행하고 /home/index/1 URL을 호출하여 다음과 같은 응답이 생성되는지를 확인합니다.
● Tag Helper를 사용한 View Component적용
Razor View와 Page는 C# class에서 관리되는 사용자정의 HTML 요소인 Tag helper를 포함할 수 있으며 view component는 tag helper로 구현되는 HTML element를 사용해 적용할 수 있습니다. 이 기능을 사용하기 위해 Views folder에 있는 _ViewImports.cshtml file을 변경하여 아래 지시자를 추가합니다.
@using MyWebApp.Components
@addTagHelper *, MyWebApp
새로운 지시자는 Project를 위한 tag helper 지원을 추가하는 것으로 예제의 Project이름인 MyWepApp으로 특정됩니다. 아래 예제에서는 Views/Home folder의 Index.cshtml에 view component를 적용하기 위해 사용자 정의 HTML 요소를 사용하도록 하였습니다.
@section Summary {
<div class="bg-info text-white m-2 p-2">
<vc:city-summary />
</div>
}
● Razor Page에서의 View Component적용
Razor Page에서는 Component속성 혹은 사용자 정의 HTML 요소를 사용했던 것과 같은 방법으로 View component를 사용할 수 있습니다. 또한 Razor Page도 자체 View Imports file을 가지고 있으므로 Pages folder에 있는 _ViewImports.cshtml 역시 @addTagHelper지시자를 사용하고
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, WebApp
Pages folder의 Data.cshtml에도 동일한 방법으로 CitySummary view component를 적용합니다.
<div class="bg-info text-white m-2 p-2">
<vc:city-summary />
</div>
위의 예는 Razor Page에서의 view component를 적용하기 위한 설명에 불과합니다. Data.cshtml과 view component는 서로 다른 유형의 Data를 취급하므로 결과는 표시되지 않습니다.
4. View Component Result
View나 Page에 간단한 문자열을 추가하는 것은 그렇게 특별한것은 아니지만 View component는 그 이상의 더 많은 것을 수행할 수 있습니다. 더욱더 복잡한 구현은 IViewComponentResult interface를 구현한 개체를 반환하는 Invoke 또는 InvokeAsync method를 가짐으로써 이룰 수 있습니다. 여기에는 IViewComponentResult interface를 구현하는 3개의 내장 class가 있으며 ViewComponent base class에서 제공되는 이들을 생성하기 위한 method와 함께 아래 표에서 간략하게 설명하고 있습니다.
ViewViewComponentResult | 선택적 view model data와 함께 Razor View를 특정하기 위한 class로서 해당 class의 instance는 view method를 사용해 생성합니다. |
ContentViewComponentResult | HTML 문서와의 통합을 위해 안전하게 encode되는 text result를 특정하는데 사용되는 class입니다.해당 class의 instance는 Content method를 사용해 생성합니다. |
HtmlContentViewComponentResult | encode없이 HTML문서에 포함되는 HTML 요소를 특정하는데 사용되는 class로서 해당 유형의 결과를 만들기 위한 ViewComponent method는 존재하지 않습니다. |
두가지 결과 유형에 대한 특별한 처리가 존재하는데 view component가 string을 반환한다면 이전 예제에 의존한 것 관련된 ContentViewComponentResult 개체를 생성하는 데 사용됩니다. 아니면 view component가 IHtmlContent개체를 반환하는 경우 이것은 HtmlContentViewComponentResult개체를 생성하는 데 사용될 수 있습니다.
(1) Partial View 반환
가장 유용한 응답으로는 다소 이상하게 명명된 ViewViewComponentResult개체인데 이 것은 Razor에게 Partial View를 Render할것과 parent view에서 결과를 포함하도록 하는 개체입니다. 여기서 ViewComponent base class는 ViewViewComponentResult개체를 생성하기 위한 5가지 version의 View method를 제공하고 있습니다.
View() | view component를 위한 기본 view를 선택하는데 사용하며 view model을 제공하지 않습니다. |
View(model) | 기본 view를 선택하는데 사용하며 view model로서의 개체를 특정합니다. |
View(viewName) | 특정한 view를 선택하는데 사용하며 view model을 제공하지 않습니다. |
View(viewName, model) | 특정한 view를 선택하며 view model로서 개체를 특정하는데 사용합니다. |
이들 method는 Controller base class에서 제공하는 것들과 일치하며 거의 같은 방법으로 사용됩니다. view component가 사용할 수 있는 view model을 생성하기 위해 CityViewModel.cs이름의 file을 Project의 Model folder에 아래와 같이 추가합니다.
namespace MyWebApp.Models
{
public class CityViewModel
{
public int? Cities { get; set; }
public int? Population { get; set; }
}
}
그리고 CitySummary view component의 Invoke method를 변경하여 parital view를 선택하는 View method를 사용하도록 하고 CityViewModel 개체를 사용하는 view data를 제공합니다.
public CitySummary(CitiesData cdata)
{
data = cdata;
}
public IViewComponentResult Invoke()
{
return View(new CityViewModel
{
Cities = data.Cities.Count(),
Population = data.Cities.Sum(c => c.Population)
});
}
현재는 view component에서 사용할 수 있는 view는 없으므로 Project를 실행하고 /home/index/1 URL을 요청하면 다음과 같이 검색 위치를 드러내는 error message를 생성하게 됩니다. /data 요청에서도 마찬가지로 view component가 Razor Page를 사용할 때 검색한 위치를 나타내는 error를 표시합니다.
Default.cshtml이름의 view를 Razor는 view component가 특정한 이름없이 view method를 호출할 때 검색하게 되는데 view component가 controller에서 사용되면 검색위치는 다음과 같이 정해지게 됩니다.
/Views/[controller]/Components/[viewcomponent]/Default.cshtml /Views/Shared/Components/[viewcomponent]/Default.cshtml /Pages/Shared/Components/[viewcomponent]/Default.cshtml |
CitySummary component가 Home controller에서 선택된 View에서 Render될때, 예를 들어 [controller]가 Home이고 viewcompnent가 CitySummary라면 이것은 첫 번째 검색위치가 /Views/Home/Components/CitySummary/Default.cshtml이 된다는 것을 의미합니다. view component가 Razor Page에서 사용되면 검색위치를 아래와 같이 될 수 있습니다.
/Pages/Components/[viewcomponent]/Default.cshtml /Pages/Shared/Components/[viewcomponent]/Default.cshtml /Views/Shared/Components/[viewcomponent]/Default.cshtml |
Razor Page의 검색경로가 Page의 이름을 포함하지 않고 Sub folder에서 정의되었다면 Razor view engine은 Razor Page가 정의된 위치에서 상대적인 Components/[viewcomponent] folder부터 view를 찾게 되고 folder계층구조상 점차 상위로 올라가면서 View가 발견되거나 Pages folder에 도달할 때까지 검색을 시도합니다.
Razor Page에서 사용된 view component는 Views/Shared/ Components folder에서 정의된 view를 찾고 controller안에서 정의된 view component는 Pages/Shared/ Components folder에서 view를 찾게 됨에 주의해야 합니다. 이 말은 view component가 Controller와 Razor page모두에서 사용될 때 view를 중복시킬 필요가 없다는 것을 의미하기도 합니다.
따라서 project에서 WebApp/Views/Shared/Components/CitySummary folder를 생성하고 Default.cshtml 이름의 Razor view를 아래와 같이 추가합니다.
@model CityViewModel
<table class="table table-sm table-bordered text-white bg-secondary">
<thead>
<tr><th colspan="2">City별 인구</th></tr>
</thead>
<tbody>
<tr>
<td>Cities:</td>
<td class="text-right">@Model?.Cities</td>
</tr>
<tr>
<td>Population:</td>
<td class="text-right">@Model?.Population?.ToString("#,###")</td>
</tr>
</tbody>
</table>
view component의 VIew는 partial view와 비슷하며 @model 지시자를 사용해 view model 개체의 유형을 설정하고 있습니다. 해당 view는 CityViewModel개체를 HTML Table의 Cell을 채우는데 사용되는 view component로부터 전달받습니다. project를 실행하여 /home/index/1로의 URL을 요청하면 view가 응답에 포함된 다음과 같은 결과를 볼 수 있습니다.
(2) HTML 조각 반환
ContentViewComponentResult class는 view의 사용없이 parent view에서 HTML조각을 포함시키기 위해 사용됩니다. ContentViewComponentResult class의 instance는 ViewComponent base class로부터 상속된 Content method를 통해 생성되며 문자열 값을 수용하는 method입니다. 아래 예제는 Components folder에 있는 CitySummary.cs에서 content method가 사용된 경우를 나타내고 있습니다.
Content method외에도 Invoke method 또한 자동적으로 ContentViewComponentResult로 변환되는 문자열을 반환할 수 있는데 이것은 실제 view component를 처음 정의할 때도 사용되었습니다.
public IViewComponentResult Invoke()
{
//return View(new CityViewModel
//{
// Cities = data.Cities.Count(),
// Population = data.Cities.Sum(c => c.Population)
//});
return Content("<h3><i>문자열</i></h3> 반환됨");
}
Content method에 전달된 문자열은 HTML문서안에서 안전하게 포함될 수 있게 encode 됩니다. 이 것은 사용자나 외부 시스템 등에서 제공된 data를 처리할 때 특히 중요한데 Application에서 생성된 HTML안으로 javascript content가 무단으로 내장되는 걸 차단할 수 있습니다.
예제에서는 몇몇 기본적인 HTML tag를 포함한 문자열을 content method로 전달하였습니다. project를 실행하고 /home/index/1 URL을 요청하면 아래와 같은 결과를 볼 수 있습니다.
결과에서의 HTML은 Tag자체로서 그대로 표현되므로 Browser는 HTML요소를 content로서 변환하지 않게됩니다. 그런데 만약 제공되는 data를 신뢰하고 있고 고의적으로 HTML로서의 변환을 원한다면 endcode가 더 이상 필요하지 않을 수도 있을 것입니다. 이런 경우 HtmlContentViewComponentResult개체를 직접적으로 생성하고 안전하다고 생각되거나 이미 encode 되었다고 확신되는 경우 문자열을 HTML로서 표현하는 HtmlString개체를 생성자에 제공해야 합니다.
using MyWebApp.Models;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Html;
...생략
public IViewComponentResult Invoke()
{
//return View(new CityViewModel
//{
// Cities = data.Cities.Count(),
// Population = data.Cities.Sum(c => c.Population)
//});
//return Content("<h3><i>문자열</i></h3> 반환됨");
return new HtmlContentViewComponentResult(new HtmlString("<h3><i>문자열</i></h3> 반환됨"));
}
이러한 처리 방법은 다소 위험성을 동반하게 되므로 조작될 수 없다고 판단되는 source와 자체 encode를 수행하는 경우에만 사용되어야 합니다. project를 실행하고 /home/index/1 URL을 요청하면 다음과 같이 encode되지 않고 HTML요소로 변환된 결과를 보게 됩니다.
5. context data 가져오기
현재 요청과 Parent View에 대한 상세정보는 ViewComponent base class에서 정의된 아래 표에서와 같은 속성을 통해 view component로 제공될 수 있습니다.
HttpContext | 이 속성은 현재 요청과 준비중인 응답을 묘사하는 HttpContext개체를 반환합니다. |
Request | 이 속성은 현재 HTTP요청을 묘사하는 HttpRequest개체를 반환합니다. |
User | 이 속성은 현재 사용자를 묘사하는 IPrincipal개체를 반환합니다. |
RouteData | 이 속성은 현재 요청에 대한 route data를 묘사하는 RouteData개체를 반환합니다. |
ViewBag | 이 속성은 view component와 view사이에서 data전달에 사용될 수 있는 dynamic view bag 개체를 반환합니다. |
ModelState | 이 속성은 model binding 처리에 관한 상세를 제공하는 ModelStateDictionary를 반환합니다. |
ViewData | 이 속성은 view component에서 제공된 view data로의 접근을 제공하는 ViewDataDictionary를 반환합니다. |
context data는 data가 선택되는 방식 또는 다른 content나 view를 render하는 것을 포함하여 view component가 동작하는데 도움이 되는 어떠한 방법으로도 사용될 수 있습니다. 이것은 각 project마다 해결하는 방법이 모두 다를 수 있으므로 view component안에서 data context의 사용을 표현하는 예제를 고안하기에는 어렵습니다. 아래 예제에서는 Components folder에 있는 CitySummary.cs file의 Invoke method를 변경하여 controller segment 변수(routing pattern이 controller나 view에서 처리될 수 있는 요청을 나타내기 위한)를 포함하는지 여부를 판단하기 위해 route data를 확인하고 있습니다.
public string Invoke()
{
if (RouteData.Values["controller"] != null)
{
return "Controller 요청";
}
else
{
return "Razor Page 요청";
}
}
project를 실행하여 /home/index/1 URL을 요청하면 해당 요청에 맞는 view component가 표시됨을 확인할 수 있습니다.
(1) 인수를 사용한 Parent view로 부터 Context제공하기
Parent View에서는 추가적인 context data를 View component에 제공함으로써 처리될 content에 대한 지침 또는 data를 함께 제공할 수 있습니다. context data는 Invoke 혹은 InvokeAsync method를 통해 아래와 같은 방법으로 전달됩니다.
public CitySummary(CitiesData cdata)
{
data = cdata;
}
public IViewComponentResult Invoke(string themeName)
{
ViewBag.Theme = themeName;
return View(new CityViewModel
{
Cities = data.Cities.Count(), Population = data.Cities.Sum(c => c.Population)
});
}
예제에서 Invoke method는 themeName이라는 매개변수를 정의하고 있으며 ViewBag을 사용해 Partial view로 값을 전달하고 있습니다. 그리고 아래 예제에서는 Default view를 변경하여 이렇게 전달받은 값을 사용해 생성되는 content의 style을 구현하는 데 사용하도록 하였습니다.
<table class="table table-sm table-bordered text-white bg-@ViewBag.Theme">
View component의 Invoke나 InvokeAsync에서 정의된 모든 매개변수 값은 반드시 제공되어야 합니다. 따라서 Views/Home folder의 Index.cshtml에서는 themeName매개변수의 값을 아래와 같이 제공하도록 하였습니다.
@section Summary {
<div class="bg-info text-white m-2 p-2">
<vc:city-summary theme-name=”secondary” />
</div>
}
각 매개변수의 이름은 kebab-case를 사용한 attribute로 표현되었습니다. 따라서 theme-name attribute는 themeName매개변수로 값을 제공하게 됩니다.
Project를 실행하여 /home/index/1 URL을 요청하면 다음과 같은 결과를 볼 수 있게 됩니다.
Component helper를 사용한 값 전달하기
Component.InvokeAsync helper를 사용한 Component view를 선호한다면 아래와 같이 method 인수를 사용한 context를 제공할 수 있씁니다.
@await Component.InvokeAsync("CitySummary", new { themeName = "danger" })
InvokeAsync method의 첫 번째 인수는 View component class의 이름이며 두 번째 인수는 view component에서 정의된 매개변수에 해당하는 이름의 개체입니다.
● 기본 매개변숫값 사용
기본값은 Invoke method의 매개변수에서 아래와 같이 정의될 수 있으며 Parent view에서 값을 제공하지 않는 경우 대체값을 제공할 수 있습니다.
public IViewComponentResult Invoke(string themeName = "success")
예제는 CitySummary.cs의 Invoke method를 변경한 것이며 기본값으로 success를 지정하였고 view component가 theme-name attribute 없이 적용될 경우 해당 값이 사용될 것입니다.
<vc:city-summary />
project를 실행하여 /home/index/1 URL을 호출하면 다음과 같은 결과를 볼 수 있습니다.
(2) 비동기 View component
지금까지의 모든 예제는 동기화 view component를 생성한 것으로 Invoke method를 정의함으로써 동기화가 될 수 있습니다. 그런데 만약 view component가 비동기 API에 의존하는 거라면 Task를 반환하는 InvokeAsync method를 정의함으로서 view component를 비동기로 생성할 수 있습니다. Razor는 InvokeAsync method로부터 Task를 전달받게 되면 작업이 완료될 때까지 대기한 다음 결과를 main view에 삽입하게 됩니다. 새로운 비동기 component를 추가하기 위해 PageSize.cs이름의 file을 아래와 같이 Components folder에 추가합니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Views.Shared.Components
{
public class PageSize : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("http://cliel.com");
return View(response.Content.Headers.ContentLength);
}
}
}
예제에서 InvokeAsync method는 HttpClient class에서 제공하는 비동기 API를 사용하기 위해 async와 await keyword를 사용하고 있고 이때 cliel.com으로 GET요청울 보내 반환된 content의 길이값을 가져오고 있습니다.
Views/Sahred/Components/PageSize folder를 생성하고 Default. cshtml이름의 Razor View를 아래와 같이 추가합니다.
@model long
<div class="m-1 p-1 bg-light text-dark">CLIEL.COM Page size: @Model</div>
마지막으로 component를 사용하기 위해 Home controller에서 사용되는 Index View를 아래와 같이 변경하였습니다. 이때 비동기 view component를 사용하기 위해 필요한 추가적인 변경사항은 없습니다.
@section Summary {
<div class="bg-info text-white m-2 p-2">
<vc:city-summary />
<vc:page-size />
</div>
}
project를 실행하고 /home/index/1 URL을 요청하면 cliel.com의 main page size를 다음과 같이 확인할 수 있습니다.
6. View Component Class
View component는 때로 Controller나 Razor에서 심층적으로 처리되는 기능의 요약 또는 snapshot을 제공하기도 합니다. 예를 들어 Shopping mall에서 장바구니를 요약하여 보여주는 View component의 경우 상품 List의 상세정보를 제공하는 Controller와 연결되고 이를 통해 장바구니 목록을 확인한 후 해당 상품의 주문을 완료하게 됩니다.
이와 같은 상황을 위해 Razor Page나 Controller뿐만 아니라 View component를 위한 class로도 생성할 수 있습니다. Project에 이미 생성한 file 중 Cities.cshtml의 class file인 Cities.cshtml.cs file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using MyWebApp.Models;
namespace MyWebApp.Pages
{
[ViewComponent(Name = "CitiesPageHybrid")]
public class CitiesModel : PageModel
{
public CitiesModel(CitiesData cdata)
{
Data = cdata;
}
public CitiesData? Data { get; set; }
[ViewComponentContext]
public ViewComponentContext Context { get; set; } = new();
public IViewComponentResult Invoke()
{
return new ViewViewComponentResult()
{
ViewData = new ViewDataDictionary<CityViewModel>(Context.ViewData, new CityViewModel { Cities = Data?.Cities.Count(), Population = Data?.Cities.Sum(c => c.Population) })
};
}
}
}
예제 page model class는 ViewComponent라는 attribute가 적용되어 있는데 이를 통해 View component로서 사용될 수 있게 되었습니다. 이때 Name 인수에는 VIew component로서 적용될 이름을 지정합니다. Page model은 ViewComponent base class로부터 상속될 수 없으므로 유형이 ViewComponentContext인 속성에는 ViewComponentContext attribute가 적용됩니다. 여기서 ViewComponentContext는 Invoke 혹은 InvokeAsync method가 호출되기 이전에 '5. context data 가져오기'에서 소개한 표의 속성들을 정의하는 object가 할당될 수 있음을 나타냅니다. 위와 같이 page model을 추가하게 되면 더 이상 View method는 사용이 불가능하므로 적용된 property를 통해 전달받은 context개체에 의존하는 ViewViewComponentResult개체를 생성해야 합니다. 다음으로 새로운 page model class사용하기 위해 page의 view부분을 아래와 같이 변경합니다.
@page
@model MyWebApp.Pages.CitiesModel
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
@foreach (City c in Model.Data?.Cities ?? Enumerable.Empty<City>())
{
<tr>
<td>@c.Name</td>
<td>@c.Country</td>
<td>@c.Population</td>
</tr>
}
</tbody>
</table>
</div>
예제에서는 page model class를 사용하기 위해 지시자를 변경하였습니다. 마지막으로 hybrid view component를 위한 view를 생성하기 위해 Pages/Shared/Components/CitiesPageHybrid folder를 만들고 Default.cshtml이름의 Razor View를 아래와 같이 추가합니다.
@model CityViewModel
<table class="table table-sm table-bordered text-white bg-dark">
<thead>
<tr>
<th colspan="2">Hybrid Page Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cities:</td>
<td class="text-right">@Model?.Cities</td>
</tr>
<tr>
<td>Population:</td>
<td class="text-right">
@Model?.Population?.ToString("#,###")
</td>
</tr>
</tbody>
</table>
Project를 실행하고 /cities URL을 요청하면 page model로서 동작하는 결과를 다음과 같이 볼 수 있습니다.
아래 예제는 hybrid class의 view component부분을 다른 page에 적용한 예를 보여주고 있습니다. Hybrid는 다른 view component와 동일하게 적용될 수 있으며 view component로서 동작하게 됩니다.
<div class="bg-info text-white m-2 p-2">
<vc:cities-page-hybrid />
</div>
(1) Hybrid Controller Class 생성
이와 같은 기술은 Controller class에도 적용될 수 있습니다. CitiesController.cs이름의 file을 Controllers folder에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using MyWebApp.Models;
namespace WebApp.Controllers
{
[ViewComponent(Name = "CitiesControllerHybrid")]
public class CitiesController : Controller
{
private CitiesData data;
public CitiesController(CitiesData cdata)
{
data = cdata;
}
public IActionResult Index()
{
return View(data.Cities);
}
public IViewComponentResult Invoke()
{
return new ViewViewComponentResult()
{
ViewData = new ViewDataDictionary<CityViewModel>(ViewData, new CityViewModel { Cities = data.Cities.Count(), Population = data.Cities.Sum(c => c.Population) })
};
}
}
}
controller가 instance화 되는 방식에서 특이한 점은 ViewComponentContext attribute의 적용이 필요하지 않다는 것을 의미하며 Controller base class로부터 파생된 ViewData속성이 view component 결과를 생성하기 위해 사용된다는 것입니다.
Action method에 View를 제공하기 위해서 Project의 Views folder하위에 Cities folder를 추가하고 그 안에 Index.cshtml file을 아래와 같이 생성합니다.
@model IEnumerable<City>
@{
Layout = "_ImportantLayout";
}
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
@foreach (City c in Model ?? Enumerable.Empty<City>())
{
<tr>
<td>@c.Name</td>
<td>@c.Country</td>
<td>@c.Population</td>
</tr>
}
</tbody>
</table>
</div>
이번에는 View component에 View를 제공하기 위해 Views/Shared/Components foler하위에 CitiesControllerHybrid folder를 추가하고 그 안에 Default.cshtml이름의 Razor View를 아래와 같이 생성합니다.
@model CityViewModel
<table class="table table-sm table-bordered text-white bg-dark">
<thead>
<tr>
<th colspan="2">Hybrid Controller Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cities:</td>
<td class="text-right">@Model?.Cities</td>
</tr>
<tr>
<td>Population:</td>
<td class="text-right">
@Model?.Population?.ToString("#,###")
</td>
</tr>
</tbody>
</table>
Project를 실행한 후 /cities/index URL을 호출하면 Controller에서의 결과를 다음과 같이 확인할 수 있습니다.
아래 예제는 Data.cshtml Razor Page에 hybrid view component를 적용한 것으로서 이전에 만들어진 hybrid class를 바꾼 것이며 view component에서 Hybrid Controller Class를 어떻게 사용할 수 있을지를 볼 수 있습니다.
<div class="bg-info text-white m-2 p-2">
<vc:cities-controller-hybrid />
</div>