ASP.NET Core - 11. View와 Controller - 2
이번 글에서는 View와 Controller 1에 이어서 Razor View에서 제공되는 더 많은 기능들에 대해 살펴보고자 합니다. 구체적으로는 View Bag을 사용해 어떻게 필요한 Data를 View로 전달할 수 있을지, View에서 중복되는 부분을 해결할 수 있는 Layout과 Layout section에 대한 사용, 그리고 어떻게 표현식으로부터의 결과를 Encoding하고 또 Encoding처리를 막을 수 있는지에 대한 것들을 하나씩 알아보도록 하겠습니다.
1. Project 준비하기
예제를 위한 Project는 이전의 Project를 그대로 사용할 것이며 다만 HomeController.cs file을 아래 내용으로 조정하여 시작할 것입니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
readonly NorthwindContext _context;
public HomeController(NorthwindContext context)
{
_context= context;
}
public async Task<IActionResult> Index(int id = 1)
{
var result = await _context.Products.FindAsync(id);
if (result?.CategoryId == 1)
return View("Watersports", result);
else
return View(result);
}
public IActionResult List()
{
return View(_context.Products);
}
}
}
또한 앞으로 설명하게될 내용에 따라 Project에 Seesion을 필요로 하므로 Program.cs file을 아래와 같이 변경하여 Session에 필요한 Service를 추가하도록 합니다.
builder.Services.AddControllersWithViews();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options => {
options.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseStaticFiles();
app.UseSession();
2. View Bag
Action Method는 View Model을 통해 View에게 무엇인가를 표시하기 위한 Data를 제공할 수 있지만 때로는 또 다른 추가적인 무엇인가를 더 전달해야 하는 경우도 있습니다. 이때 Action Method는 View Bag을 사용해 여기서 필요한 정보를 제공해 줄 수 있습니다.
public async Task<IActionResult> Index(int id = 1)
{
ViewBag.AveragePrice = await _context.Products.AverageAsync(p => p.UnitPrice);
return View(await _context.Products.FindAsync(id));
}
ViewBag속성은 Controller Base class로 부터 상속되며 dynamic개체를 반환합니다. 이를 통해 Action Method는 필요한 값을 할당하기 위한 새로운 속성을 생성할 수 있고 Action Method에서 ViewBag속성에 할당된 값은 View에서 같은 속성을 사용함으로써 해당 값을 가져올 수 있게 됩니다. 이에 따른 예를 index.cshtml에서 아래와 같이 보여주고 있습니다.
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th>
<td>
@Model?.UnitPrice.ToString("c")
(@(((Model?.UnitPrice / ViewBag.AveragePrice) * 100).ToString("F2"))% of average price)
</td>
</tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
VIewBag속성은 개체를 Action Method에서 View로 View Model개체와 함께 전달하게 됩니다. 예제에서 Action Method는 Database에 Products개체의 UnitPrice의 평균값을 질의하고 그 결과를 AveragePrice라는 이름의 ViewBag속성에 할당한뒤 다시 View에서 해당 속성을 표현식을 통해 값을 사용하고 있습니다. Project를 실행하고 아래와 같은 응답이 발생하는지를 확인합니다.
ViewBag을 사용해야 하는 경우
ViewBag은 Action Method에서 별도의 View Model class를 생성하지 않으면서도 비교적 적은 양의 Data를 View로 제공하고자 할때 유용하게 사용될 수 있습니다. ViewBag의 단점은 @model표현식을 사용하지 않는 View처럼 Dynamic개체에 대한 속성의 사용을 확인할 수 없다는 것입니다. 이쯤 되면 View로 Data를 전달할 때 View Model을 사용할지 ViewBag을 사용할지에 대한 판단이 필요할 수 있는데 여러 Action Method에서 동일한 View Model이 사용되거나 2개 내지 3 이상의 속성을 필요로 하는 경우에 View Model을 사용하는 것이 좋습니다.
3. Temp Data
Temp Data는 Controller가 임시로 필요한 Data를 유지할 수 있도록 할 수 있는데 특히, Redirection을 수행하는 경우 유용하게 사용될 수 있습니다. Temp data는 Session data로 저장될때 Seesion이 활성화되어 있지 않으면 Cookie를 사용해 저장됩니다. session data와 달리 temp data값은 값을 읽을 때 삭제됨을 나타내는 표시 처리가 이루어지며 요청이 처리되고 나면 그때서야 비로소 삭제됩니다.
이에 대한 예를 만들어 보기위해 Project의 Controllers folder에 CubeController.cs file을 아래와 같이 생성합니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Controllers
{
public class CubeController : Controller
{
public IActionResult Index()
{
return View("Cubed");
}
public IActionResult Cube(double d)
{
TempData["value"] = d.ToString();
TempData["result"] = Math.Pow(d, 3).ToString();
return RedirectToAction(nameof(Index));
}
}
}
예제에서는 Cubed이름의 View를 선택하도록 하는 Index Method를 정의하였고 요청으로부터 d매개변수 값을 가져오기 위한 Model binding처리를 가진 Cube Action Method를 정의하였습니다. Cube action method에서는 매개변수 d의 값과 Math의 Row Method결괏값을 저장하기 위해 TempData를 사용하고 있는데 어기서 TempData는 key-value쌍의 값을 저장하기 위한 dictionary를 반환합니다. TempData기능은 또한 Session기능 위에 구축되며 문자열로 serialize될 수 있는 것만이 저장될 수 있으므로, 따라서 예제에서는 double형식의 값을 문자열로서 저장하게 되었습니다. 일단 값이 TempData로 저장되고 나면 Cube auction method는 Index로 redirection을 수행하게 되는데 이에 필요한 View를 제공하기 위해 /Views/Shared folder에서 Cubed.cshtml file을 아래와 같이 추가해 줍니다.
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h6 class="bg-secondary text-white text-center m-2 p-2">Cube</h6>
<form method="get" action="/cube/cube" class="m-2">
<div class="form-group">
<label>Value</label>
<input name="d" class="form-control" value="@(TempData["value"])" />
</div>
<button class="btn btn-primary mt-1" type="submit">Submit</button>
</form>
@if (TempData["result"] != null)
{
<div class="bg-info text-white m-2 p-2">
The cube of @TempData["value"] is @TempData["result"]
</div>
}
</body>
</html>
Razor View에 사용되는 Base class는 TempData속성을 통해 TempData로의 접근을 제공하고 있으며 이를 통해 표현식에서 해당 TempData의 값을 읽을 수 있습니다. 예제의 경우 input요소를 통해 content의 값을 설정하고 결과를 표시하는데 TempData를 사용하였습니다. TempData는 값을 읽게되면 그 즉시 값이 제거되지 않습니다. 같은 VIew에서 해당 TempData의 값을 반복적으로 읽을 수 있기 때문인데 일단 요청이 처리되고 나면 표시된 값은 제거될 것입니다.
Project를 실행하여 /cube로 URL을 요청합니다.
그리고 input에 값을 입력하고 Submit button을 누르면 Browser는 TempData값을 설정하는 요청을 보내게 되고 redirection을 수행하게 될 것입니다. TempData값은 새로운 Redirection요청에도 계속 보존될 수 있기 때문에 사용자에게 해당 결과를 표시할 수 있게 됩니다. 하지만 Data값을 읽고난 이후에는 삭제 표시가 이루어지므로 Browser를 새로고침 하게 되면 input의 입력 content와 결과는 더 이상 표시되지 않게 됩니다.
TempData속성을 통해 반환된 개체는 Peek Method를 제공하여 삭제표시없이 Data를 읽을 수 있도록 지원하고 있습니다. 또한 Keep Method를 통해 이전에 읽은 값이 삭제되는 걸 방지할 수도 있습니다. 다만 Keep Method라도 값을 또다시 읽게 되면 삭제가 표시되므로 값을 계속 유지시킬 수는 없습니다. 요청이 처리되고 난 이후라도 저장된 값이 삭제되지 않기를 바란다면 Seesion Data를 사용해야 합니다.
TempData Attribute
Controller에서는 TempData의 attribute를 적용한 속성을 정의할 수 있고 이것은 TempData속성을 완벽히 대체할 수 있습니다.
[TempData]
public string? Value { get; set; }
위와 같은 속성에 할당된 값은 자동적으로 TempData로 저장되며 View에서 이들에게 접근하는 방식에는 차이가 없습니다. 어떤 경우에는 Controller에 Action Method만을 남겨둘 목적으로 TempData의 사용을 더 선호하기도 하지만 2가지 방법 모두 완전히 유효한 것이며 어떤 것을 선택하느냐는 선호도의 문제일 뿐입니다.
4. Layout
예제에서 사용된 View는 Head영역을 정의하거나 Bootstrap CSS file을 loading하는 것과 같은 HTML 문서의 설정을 위한 중복된 요소를 포함하고 있습니다. 이러한 상황을 위해 Razor에서는 Layout을 지원하고 있으며 이에 따라 모든 View에서 사용 가능한 단일 file에서 공통된 content를 통합시킬 수 있습니다.
Layout은 대게 하나 이상의 Controller에서 여러 Action Method를 통해 사용될 수 있으므로 Views -> Shared foler에 저장됩니다. Layout file을 추가하려면 Views의 Shared folder에서 Mouse오른쪽 button을 눌러 Add -> New Item을 선택합니다. 그런 후 Template화면에서 아래와 같이 Razor Layout을 선택하고 'Add' Button을 눌러줍니다.
그러면 _Layout.cshtml은 아래 내용으로 생성될 것입니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
Layout에는 여러 View에서 사용될 수 있는 공통된 content를 포함해야 합니다. 각 View를 위한 독자적인 content는 RazorPage<T> class에서 상속된 RenderBody() Method를 호출함으로써 응답에 포함될 수 있습니다. Layout에서 사용되는 View는 그저 자신이 가진 content에만 집중하면 됩니다. Layout file을 사용하기 위해 Index.cshtml을 아래와 같이 수정하여 Layout을 지정해 줍니다.
@{
Layout = "_Layout";
}
<!DOCTYPE html>
<html>
Layout은 @{와 }로 표시된 code block에서 Layout 속성을 통해 지정됩니다. 예제의 경우 Layout 속성의 값으로 위에서 추가한 _Layout file명이 지정되었습니다. 보통 View에서 Layout이 지정되는 경우에는 확장자나 file의 경로 없이 이름만으로 설정되며 Razor engine은 같은 이름의 file을 찾기 위해 /Views/[Controller]와 /Views/Shared folder를 검색하게 됩니다.
(1) View Bag을 사용한 Layout설정
View는 Layout에게 Data값을 제공할 수 있으며 View에 의해 제공된 공통 content를 사용자 정의할 수 있습니다. 이때 필요한 ViewBag은 Layout을 지정하는 code영역에 아래와 같이 정의됩니다.
@{
Layout = "_Layout";
ViewBag.Title = "환영합니다.";
}
예제에서는 Title속성을 설정하고 있는데 해당 속성의 값은 이미 Layout을 생성할때 Title의 content로 사용될 수 있도록 지정되었습니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h6 class="bg-primary text-white text-center m-2 p-2">
@(ViewBag.Title ?? "Layout")
</h6>
@RenderBody()
</body>
</html>
여기에서 더 나아가 h6 요소를 body에 위와 같이 추가해 줍니다. 다만 Layout은 정의 중인 ViewBag속성에 의존할 수 없으므로 View에서 Title속성을 정의하지 않고 있는 경우 이를 대응하기 위한 대체 값을 제공해 주는 것이 좋습니다. Project를 실행하여 아래와 같은 응답이 생성되는지를 확인합니다.
ViewBag 우선 순위
View와 Action Method에서 정의한 ViewBag속성이 같은 경우 View에서 정의된 값이 더 높은 우선권을 가질 수 있습니다. 따라서 만약 Action Method에서 정의된 ViewBag의 값을 View의 ViewBag값으로서 사용하려면 아래와 같은 방식을 사용해
@{
Layout = "_Layout";
ViewBag.Title = ViewBag.Title ?? "제품목록";
}
View에서 Title속성의 값이 정의되지 않은 경우에 '제품 목록'과 같은 값이 사용될 수 있도록 해야 합니다.
(2) View Start File
모든 View에 Layout을 설정하는 대신 Project에 View Start file을 추가하여 기본 Layout을 제공해 줄 수 있습니다.
위와 같이 Razor View Start항목을 선택하고 file을 추가하면 아래 content를 포함한 _ViewStart.cshtml이름의 file이 추가됩니다.
@{
Layout = "_Layout";
}
예제에서는 _Layout이라는 값으로 Layout속성을 설정하고 있습니다. 해당 file을 변경하지 않고 Project에서 Common.cshtml file을 찾아 아래와 같이 변경합니다.
<h6 class="bg-secondary text-white text-center m-2 p-2">Shared View</h6>
위의 예제는 View model type을 정의하고 있지 않으며 Layout설정 또한 필요로 하지 않습니다. 왜냐하면 위에서 Project에 View Start file을 추가했기 때문입니다. 따라서 응답의 body영역에 위 HTML 요소가 추가될 것입니다. Project를 실행해 /second URL을 요청하여 아래와 같은 응답이 표시되는지 확인합니다.
(3) 기본 Layout 재정의 하기
View start file이 Project에 존재하는 상황에서도 View에서 Layout을 정의해야 하는 상황이 생길 수 있습니다.View Start file에 정의된 것이 아닌 View에서 다른 Layout을 필요로 하는 상황은 언제든 발생할 수 있는 것입니다. Views->Shared folder에서 아래와 같이 _ImportantLayout.cshtml이름의 file을 추가합니다.
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h3 class="bg-warning text-white text-center p-2 m-2">Important</h3>
@RenderBody()
</body>
</html>
HTML 문서를 구조화하기 위해 예제에서는 Important라는 큰 글자를 표시하는 Header요소를 포함하고 있습니다. View에서는 Layout속성의 값을 위 file이름으로 설정함으로써 필요한 Layout을 선택할 수 있습니다. 아래 예제에서는 Index.cshtml file에서 상기 Layout을 지정하였습니다.
만약 단일 Controller에 있는 모든 Action에서 다른 Layout을 사용해야 한다면 View Start file을 Views->[Controller] foler에 추가합니다. 그러면 Razor engine은 Controller별 View file에 존재하는 Layout을 사용하게 됩니다.
@{
Layout = "_ImportantLayout";
ViewBag.Title = "환영합니다.";
}
기존 View Start file에서 지정된 Layout값은 View에서 재정의하였으며 이로 인해 기존과 다른 Layout이 Index.cshtml에 적용될 것입니다. Project를 실행해 아래와 같은 응답이 생성되는지 확인합니다.
Program에 따라 Layout 선택하기
View가 Layout속성에 할당한 값은 Layout이 View에서 선택될 수 있도록 하는 표현식의 결과가 될 수 있습니다. 비슷한 방법으로 Action Method에서도 View를 선택할 수 있습니다. 예를 들어 아래와 같이 View Model에 정의된 속성 값을 통해 Layout을 선택하는 것도 가능합니다.
Layout = Model.UnitPrice > 100 ? "_ImportantLayout" : "_Layout";
_ImportantLayout이름의 Layout은 Model 개체의 UnitPrice속성 값이 100보다 큰 경우에 선택되고 그렇지 않으면 _Layout이 사용될 것입니다.
이외 Layout을 별도로 지정해야 하는 다른 상황은 View가 자체적으로 완전한 HTML 문서를 가지고 있는 경우이며 이런 상황에서 Layout은 필요하지 않을 것입니다.
Project를 실행하여 /home/list로 URL을 요청합니다. 해당 요청은 List.cshtml의 응답을 생성하게 되는데 실제 List.cshtml은 Layout이 필요하지 않은 완전한 HTML 문서를 포함하고 있으므로 생성된 응답을 분석해 보면 아래와 같은 HTML이 생성되어 있음을 알 수 있습니다.
보이는 바와 같이 View의 완전한 HTML 요소와 Layout의 HTML요소가 결합하여 중복된 요소가 겹쳐지고 있음을 알 수 있습니다. Browser는 이와 같은 기형적인 HTML 문서를 처리하는데 아무런 문제가 없을 수 있지만 가급적이면 위와 같은 허술한 Content를 생성하지 않도록 해야 합니다. View가 완전한 HTML문서를 이루고 있다면 Layout속성을 null로 설정합니다.
@{
Layout = null;
decimal avr = Model?.Average(p => p.UnitPrice) ?? 0;
}
List.cshtml을 위와 같이 변경하고 Project를 실행한 뒤 /home/list로 URL을 다시 요청하면 아래와 같이 Layout이 포함되지 않은 잘 정리된 HTML응답을 볼 수 있습니다.
(4) Layout Section
Razor View engine은 Section이라는 개념을 지원함으로써 Layout안에서 content의 영역을 제공할 수 있도록 하고 있습니다. View의 특정 부분을 보다 효과적으로 제어할 수 있는 Razor section은 Layout안에서 지정된 곳에 추가됩니다. Section기능을 확인해 보기 위해 /Views/Home/Index.cshtml file을 아래와 같이 변경합니다.
@{
Layout = "_Layout";
ViewBag.Title = "환영합니다.";
}
@section Header {
제품 정보
}
<tr><th>제품명</th><td>@Model?.ProductName</td></tr>
<tr><th>단가</th>
<td>
@Model?.UnitPrice.ToString("c")
(@(((Model?.UnitPrice / ViewBag.AveragePrice) * 100).ToString("F2"))% of average price)
</td>
</tr>
<tr><th>카테고리 ID</th><td>@Model?.CategoryId</td></tr>
@section Footer {
평균 단가의 @(((Model?.UnitPrice / ViewBag.AveragePrice) * 100).ToString("F2")) %
}
Section은 Razor @section 표현식을 통해 이름을 지정함으로써 정의되는데 위 예제에서는 Header와 Footer라는 이름의 2개 Section이 정의되었습니다. Section도 View의 주요 부분처럼 HTML Content와 표현식이 혼합될 수 있고 Layout에서는 @RenderSection 표현식을 통해 Section을 적용합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="bg-info text-white m-2 p-1">
Layout 영역
</div>
<h6 class="bg-primary text-white text-center m-2 p-2">
@RenderSection("Header")
</h6>
<div class="bg-info text-white m-2 p-1">
Layout 영역
</div>
<div class="m-2">
<table class="table table-sm table-striped table-bordered">
<tbody>
@RenderBody()
</tbody>
</table>
</div>
<div class="bg-info text-white m-2 p-1">
Layout 영역
</div>
<h6 class="bg-primary text-white text-center m-2 p-2">
@RenderSection("Footer")
</h6>
<div class="bg-info text-white m-2 p-1">
Layout 영역
</div>
</body>
</html>
Layout이 적용되면 RenderSection표현식은 특정한 section의 content를 응답에 추가시키게 됩니다. Section에 포함되지 않은 View의 영역은 RenderBody method에 의해 응답으로 추가됩니다. Section이 적용된 결과를 확인하기 위해 Project를 실행합니다.
View는 Layout에서 참조되는 Section만 정의할 수 있습니다. 만약 layout에 해당하 @RenderSection표현식이 없는 View에 Section을 정의한다면 View engine은 예외를 발생시키게 됩니다.
Section은 View가 특별한 사용방법을 지정하지 않으면서 content의 일부 조각을 제공할 수 있도록 합니다. 아래 예제는 body와 section을 단일 HTML Table로 통합할 수 있도록 재정의한 _Layout.cshtml file을 표시하고 있습니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<table>
<thead>
<tr>
<th class="bg-primary text-white text-center" colspan="2">
@RenderSection("Header")
</th>
</tr>
</thead>
<tbody>
@RenderBody()
</tbody>
<tfoot>
<tr>
<th class="bg-primary text-white text-center" colspan="2">
@RenderSection("Footer")
</th>
</tr>
</tfoot>
</table>
</body>
</html>
위와 같이 변경한 후 Project를 다시 실행하면 아래와 같은 응답을 볼 수 있습니다.
● Optional Layout Section
기본적으로 View는 Layout에 RenderSection호출이 있는 모든 Section을 포함해야 하며 Layout에서 View가 정의하지 않은 Section을 요구하게 되면 예외를 발생시키게 됩니다. 아래 예제에서는 Views/Shared의 _Layout.cshtml을 수정하여 Summary라는 Section이 필요한 RenderSection method를 추가하였습니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<table>
<thead>
<tr>
<th class="bg-primary text-white text-center" colspan="2">
@RenderSection("Header")
</th>
</tr>
</thead>
<tbody>
@RenderBody()
</tbody>
<tfoot>
<tr>
<th class="bg-primary text-white text-center" colspan="2">
@RenderSection("Footer")
</th>
</tr>
</tfoot>
</table>
@RenderSection("Summary")
</body>
</html>
이 상태에서 Project를 실행하면 다음과 같은 예외를 보게 될 것입니다.
이 문제를 해결하는 데는 2가지 방법이 사용될 수 있습니다. 그중 첫 번째는 optional section을 생성하는 것으로 View에 정의된 것만을 Render 할 수 있도록 합니다. Optinal Section은 RenderSection Method에서 아래와 같이 2번째 매개변수를 전달함으로써 생성할 수 있습니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<table>
<thead>
<tr>
<th class="bg-primary text-white text-center" colspan="2">
@RenderSection("Header", false)
</th>
</tr>
</thead>
<tbody>
@RenderBody()
</tbody>
<tfoot>
<tr>
<th class="bg-primary text-white text-center" colspan="2">
@RenderSection("Footer", false)
</th>
</tr>
</tfoot>
</table>
@RenderSection("Summary", true)
</body>
</html>
두 번째 매개변수를 통해 해당 Section이 필수적인지 아닌지를 설정하게 되며 만약 false가 전달된다면 View가 Section을 정의하지 않았더라도 예외를 발생시키지 않게 됩니다.
● Layout Section 확인하기
IsSectionDefined Method를 사용하면 View에서 특정한 Section에 대해 정의가 되었는지의 여부를 판단할 수 있고 if 표현식을 통해 대체 content를 Render 할 수 있습니다.
@if (IsSectionDefined("Summary"))
{
@RenderSection("Summary", true)
}
else
{
<div class="bg-info text-center test-white">
대체 표시 영역
</div>
}
IsSectionDefined method는 확인하고자 하는 Section의 이름으로 호출되며 View가 해당 Section을 정의하였다면 true가 반환될 것입니다. 따라서 예제에서는 View Summary라는 Section을 정의하고 있지 않으므로 대체 content를 Render 하게 될 것입니다.
5. Partial View
Partial View는 같은 HTML 요소나 표현식을 일부 서로 다른 곳에서 반복적으로 구현해야 하는 경우 유용하게 사용될 수 있습니다. 중복되지 않으면서도 복잡한 응답을 생성해야 하는 다른 View에서 응답을 위해 포함되어야 할 content의 요소를 가지게 됩니다.
(1) Partial View 사용 준비
Partial View는 tag helper라는 기능을 통해 적용될 수 있으며 tag helper는 view import file에서 구성됩니다. Partial View를 위해 tag helper기능을 활성화하기 위해 우선 아래와 같이 _ViewImports.cshtml file에 필요한 구문을 추가해 줍니다.
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
(2) Partial View 만들기
사실 Partial View는 일반적인 cshtml인데 일반적인 다른 View와 구별되는 차이점은 이들이 사용되는 방식입니다. Partial View는 Visual Studio의 'Add New Item'에서 'Razor View' Template을 선택한 뒤 생성할 file명을 지정함으로써 추가할 수 있습니다.
file이 생성되면 해당 file을 아래와 같이 구현합니다.
@model Products
<tr>
<td>@Model?.ProductName</td>
<td>@Model?.UnitPrice</td>
</tr>
이때 model표현식은 해당 Partial view에서 사용할 view model유형을 정의하는 데 사용되었으며 일반적인 다른 View처럼 HTML 요소와 표현식이 결합된 content를 가질 수 있습니다. 해당 content는 table요소에서 row를 생성하며 해당 cell을 채우기 위해 Products개체의 ProductName과 UnitPrice속성을 사용하고 있습니다.
(3) Partial View 적용하기
Partial View는 다른 View나 Layout에서 partial요소를 추가함으로써 적용할 수 있습니다. 예제에서는 List.cshtml에 해당 요소를 적용하였으며 따라서 Partial View를 통해 Table의 Row를 생성하게 될 것입니다.
<tbody>
@foreach (Products p in Model ?? Enumerable.Empty<Products>())
{
<partial name="_RowPartial" model="p" />
}
</tbody>
partial요소에 적용된 속성은 선택을 제어하고 Partial View를 구성하게 되며 여기에는 다음표에 해당하는 속성이 사용될 수 있습니다.
name | 일반 View와 동일한 검색 process를 사용하여 위치한 Partial View의 이름을 특정합니다. |
model | Partial View에서 view model개체로 사용될 값을 특정합니다. |
for | Partial View에서 view model개체를 선택하는 표현식을 정의하는데 사용됩니다. 이 부분은 아래에서 자세히 다룰 것입니다. |
view-data | 이 속성은 추가적인 data를 Partial View에 재공하는데 사용됩니다. |
예제에서 사용된 parital요소에서는 name을 통해 '_RowPartial'View를 선택하고 있으며 model속성에서는 사용될 View Model 개체로 Products개체를 전달하고 있습니다. partial요소에서는 @foreach 표현식 안에서 적용되고 있으므로 table의 각 row를 생성하는 데 사용될 수 있는 것입니다. 따라서 Project를 실행한 후 /home/list로 URL을 요청하면 아래와 같은 응답이 생성됩니다.
● 표현식을 사용한 Partial View Model 선택하기
표현식을 통해 Partial View의 Model을 설정하는 데 사용되는 for attribute는 View의 Model에 적용되며 보다 쉽게 사용할 수 있는 기능입니다. _CellPartial.cshtml이름의 Partial View를 Views/Home folder에 아래와 같이 추가합니다.
@model string
<td class="bg-info text-white">@Model</td>
위 예제의 Partial View는 string model개체를 가지고 있는데 해당 개체는 table cell요소의 content로서 사용됩니다. 이때 table cell은 Bootstrap CSS framework를 사용한 style을 적용하였습니다. 이어서 _RowPartial.cshtml file에서는 _CellPartial.cshtml을 사용하는 partial 요소를 추가하여 Products 개체의 이름을 table cell에서 표시될 수 있도록 합니다.
@model Products
<tr>
<partial name="_CellPartial" for="ProductName" />
<td>@Model?.UnitPrice</td>
</tr>
for 속성은 _CellPartial Partial View의 model로서 ProductsName을 선택하고 있는데 이에 대한 결과는 Project를 실행하여 /home/list로 URL을 요청함으로써 확인할 수 있습니다.
Templated delegate
Templated delegate는 View에 중복을 피하기 위한 대안으로 사용될 수 있으며 아래와 같이 Code block에서 정의됩니다.
@{
Func<Products, object> row
= @<tr><td>@item.ProductName</td><td>@item.UnitPrice</td></tr>;
}
예제에서 template는 Products 개체를 수용하고 동적 결과를 반환하는 function으로서 정의되었습니다. template표현식 안에서 입력 개체는 item으로서 참조하게 되고 content를 생성하기 위해 다음과 같이 method 표현식으로 호출될 수 있습니다.
@foreach (Products p in Model ?? Enumerable.Empty<Products>())
{
//<partial name="_RowPartial" model="p" />
@row(p);
}
6. Content-Encoding
Razor View는 content를 encoding 하기 위한 유용한 2가지 기능을 제공하고 있습니다. HTML Encoding기능은 표현식이 WebBrowser로 보내는 응답의 구조를 바꾸지 않도록 하는데 이는 보안에 매우 중요한 사항입니다. JSON Encoding기능은 개체를 JSON으로 Encoding 하고 그것을 응답에 삽입하는 기능으로서 debugging에 유용하게 사용될 수 있을 뿐만 아니라 Javascript Application에 data를 제공하는 데에도 사용될 수 있습니다.
(1) HTML Encoding
Razor View engine은 표현식의 결과를 Encoding 함으로써 구조적인 변경 없이 HTML 문서 안으로 안전하게 삽입될 수 있도록 합니다. 이는 사용자에게 제공되는 content를 처리할 때 중요한 기능인데 누군가가 Application을 손상시키려 하거나 우연으로라도 위험한 content를 전달하려 하는 경우에는 더욱 그렇습니다. 아래 예제는 Home Controller에 action method를 추가하여 HTML조각을 View method로 전달하도록 하고 있습니다.
public IActionResult Html()
{
return View((object)"<h3><i>문자열</i></h3>전송");
}
위 action method는 HTML요소가 포함된 문자열을 전달하고 있으므로 이 action method에 대한 View를 Html.cshtml이름으로 Views/Home folder에 아래와 같이 생성합니다.
예제에서는 전송되는 문자열을 개체(object)로 변환하고 있음에 주목하시기 바랍니다. 이렇게 하지 않으면 문자열은 view model개체가 아닌 View의 이름으로 간주합니다.
@model string
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="bg-secondary text-white text-center m-2 p-2">@Model</div>
</body>
</html>
file을 저장하고 /home/html로 URL을 요청합니다. 결과를 다음과 같이 나올 텐데
이를 통해 view model 문자열에서 잠재적으로 위험한 문자가 어떻게 변환되는지를 볼 수 있습니다.
이번에는 표현식의 결과를 안전한 encoding 없이 포함시키기 위해 Html.Raw method를 사용하도록 변경합니다. 이 Html속성은 생성된 view class에 추가된 속성 중의 하나로서 IHtmlHelper interface를 구현한 개체를 반환하게 됩니다.
<div class="bg-secondary text-white text-center m-2 p-2">@Html.Raw(@Model)</div>
위와 같이 file을 저장하고 Project를 실행한 뒤 위와 동일한 URL을 요청하면 View model 문자열이 Encoding 없이 전달되었고 Browser에 의해 HTML 문서의 일부로 변환되었음을 알 수 있습니다.
악성 content가 View로 전달되지 않는다는 확신이 없는 한 Encoding기능을 활용하는 것이 좋습니다. 이 기능의 부주의한 사용은 Application과 사용자에게 보안적인 손실을 가져올 수 있습니다.
(2) JSON Encoding
Json 속성은 View에서 생성된 class에 추가되며 개체를 JSON으로 Encoding 하는 데 사용될 수 있습니다. JSON Data가 가장 일반적으로 사용되는 곳은 RESTful web service이긴 하지만 Razor JSON Encoding기능은 view에서 예상한 결과를 얻지 못했을 경우 debugging 하는 데에도 유용하게 사용될 수 있습니다. 아래 예제는 Index.cshtml view의 view model 개체에 JSON표현식이 추가한 것을 나타내고 있습니다.
@section Summary {
<div class="bg-info text-white m-2 p-2">
@Json.Serialize(Model)
</div>
}
Json속성은 IJsonHelper interface의 구현체를 반환하며 Serialize method는 개체에 대한 JSON표현식을 추가하게 됩니다. Project를 실행하면 다음과 같은 결과를 확인하실 수 있습니다.