'viewengine'에 해당되는 글 1건

  1. 2009.09.28 Building Custom ViewEngines on ASP.NET MVC v1.0 (2)

안녕하세요? 웹지니입니다.
이번 포스트에서는 ASP.NET MVC 1.0에서 뷰의 렌더링 방식을 관리하는 ViewEngine에 대해 알아보고 사용자 정의 ViewEngine을 구현하여 MVC 프레임워크의 뷰 렌더링 방식을 커스터마이징하는 방법에 대해 알아보겠습니다.

예제 코드 다운로드: MvcApplication1.zip (535.71 kb) 

Understanding ViewEngines

ViewEngine은 ASP.NET MVC 프레임워크에서 지정된 뷰 파일을 탐색하고 해당 뷰를 렌더링하기 위해 필요한 일련의 작업을 담당하는 컴포넌트입니다. 우선 아래 그림을 살펴볼까요?

그림 1: MVC 요청의 처리 순서
img1

위의 그림은 MSDN 매거진 2009년 7월호에 게재된 Dino Esposito의 아티클 Comparing Web Forms and ASP.NET MVC에서 발췌한 MVC 요청의 실행 다이어그램입니다. 간략히 설명하자면 먼저 웹브라우저에 의해 MVC 요청이 발생하면 URL 라우팅 엔진이 해당 요청을 분석하고 이 요청을 처리할 컨트롤러 객체의 인스턴스를 생성하게 됩니다. 그런 후 지정된 액션이 호출되고 이 액션을 처리할 컨트롤러의 액션 메서드가 실행됩니다. 이 액션 메서드가 View() 메서드나 PartialView() 메서드를 호출하여 뷰를 렌더링하고자 하면 해당 뷰를 탐색(Lookup View)하고 렌더링하는 (Render View) 과정을 거치게 되는데 이 역할을 담당하는 것이 바로 ViewEngine입니다.

IViewEngine 인터페이스

ASP.NET MVC의 ViewEngine은 IViewEngine 인터페이스를 구현하는 클래스입니다. IViewEngine 인터페이스는 다음과 같이 정의되어 있습니다.

코드 1: IViewEngine 인터페이스

public interface IViewEngine
{
    ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
    void ReleaseView(ControllerContext controllerContext, IView view);
}


위의 코드에서 보듯이 IViewEngine 인터페이스는 지정된 뷰를 찾기 위한 FindView 메서드와 FindPartialView 메서드를 정의하고 있습니다. ReleaseView 메서드는 렌더링이 완료된 뷰를 메모리에서 해제하기 위해 호출되는 메서드입니다. 따라서 IViewEngine 인터페이스를 구현하는 클래스를 정의하고 FindView 메서드나 FindPartialView 메서드만 구현하면 사용자 정의 ViewEngine을 구현할 수 있습니다.

특히 ReleaseView 메서드에는 IView 인터페이스가 매개 변수로 전달되는데 이 IView 인터페이스는 ViewEngine에 의해 관리되는 뷰 자체를 구현하기 위한 인터페이스입니다. ASP.NET MVC에는 기본적으로 WebFormView 클래스가 이 인터페이스를 구현하고 있으며 그런 이유로 웹 폼 페이지(*.aspx)나 사용자 정의 컨트롤(*.ascx)을 뷰로 활용할 수 있게 되는 것입니다. 따라서 IView 인터페이스를 구현하는 클래스를 정의한다면 이 또한 웹 폼 뷰를 대체하여 전혀 다른 형태의 뷰를 구현할 수 있게 되는 셈입니다. IView 인터페이스는 다음과 같이 정의되어 있습니다.

코드 2: IView 인터페이스

  1: public interface IView
  2: {
  3:     void Render(ViewContext viewContext, TextWriter writer);
  4: }


위의 코드에서 보듯이 IView 인터페이스는 Render라는 이름의 메서드만을 정의하고 있습니다. 따라서 그림 1에서 Lookup View 과정과 Render View 과정이 각각 ViewEngine과 View 객체에서 처리된다는 것을 알 수 있습니다.

Implementing Your Own ViewEngine

그러면 이제 새로운 ViewEngine을 직접 구현해 보도록 하겠습니다. 이번 포스트에서 작성해 볼 ViewEngine은 지정된 컨트롤러의 액션 메서드를 위한 HTML 마크업 코드를 XML 파일로부터 가져와 렌더링하는 ViewEngine입니다. 예제로 구현된 것이기에 중간에 서버 측 코드를 실행하는 등의 기능은 실행할 수 없지만 아주 불가능한 것은 아니므로 그 부분은 독자 여러분의 몫으로 남겨놓고 이번 예제에서는 ViewEngine의 구현 과정에 대해서 이해할 수 있는 수준의 예제로 구현해 보겠습니다.

우선 Visual Stduio 2008을 열고 새로운 ASP.NET MVC Web Application 프로젝트를 생성합니다. 새로 생성된 프로젝트의 솔루션 탐색기의 모습은 아래 그림과 같을 것입니다.

그림 2: ASP.NET MVC Web Application 프로젝트를 새로 생성한 모습
img2

프로젝트를 생성했으면 MvcApplication1 프로젝트의 App_Data 폴더에 Contents.xml이라는 이름의 XML 문서를 생성하고 다음과 같이 예제 콘텐츠를 작성합니다.

코드 3: Contents.xml 파일의 콘텐츠

  1: <?xml version="1.0" encoding="utf-8" ?>
  2: <contents>
  3: 	<content>
  4: 		<key>Home_Index</key>
  5: 		<method />
  6: 		<body>
  7: 			<![CDATA[
  8: 				<html>
  9: 				<body>
 10: 					<h2>Home_Index</h2>
 11: 					<a href="/Home/About">About</a>
 12: 				</body>
 13: 				</html>
 14: 			]]>
 15: 		</body>
 16: 	</content>
 17: 	<content>
 18: 		<key>Home_About</key>
 19: 		<method>POST</method>
 20: 		<body>
 21: 			<![CDATA[
 22: 				<html>
 23: 				<body>
 24: 				<h2>Home_About_Post</h2>
 25: 				</body>
 26: 				</html>
 27: 			]]>
 28: 		</body>
 29: 	</content>
 30: 	<content>
 31: 		<key>Home_About</key>
 32: 		<method>GET</method>
 33: 		<body>
 34: 			<![CDATA[
 35: 				<html>
 36: 				<body>
 37: 				<h2>Home_About_Get</h2>
 38: 				
 39: 				<form action="/Home/About" method="post">
 40: 					<input type="submit" />
 41: 				</form>
 42: 				</body>
 43: 				</html>
 44: 			]]>
 45: 		</body>
 46: 	</content>
 47: </contents>


위의 코드에서 정의된 <content> 요소는 <key>라는 이름의 요소를 가지고 있습니다. 잘 보면 이 요소의 값은 [컨트롤러_액션]의 형태를 가지고 있습니다. 뿐만 아니라 <method> 요소를 이용하여 HTTP 요청에 사용된 메서드가 GET인지 POST인지를 구분할 수 있도록 정의되어 있습니다. 이제 프로젝트에 ViewEngine이라는 이름의 폴더를 생성하고 여기에 XmlViewEngine이라는 이름의 클래스를 추가한 후 다음과 같이 코드를 작성합니다.

코드 4: XmlViewEngine 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1.ViewEngine
  8: {
  9: 	public class XmlViewEngine : IViewEngine
 10: 	{
 11: 		#region IViewEngine Members
 12: 
 13: 		public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
 14: 		{
 15: 			throw new NotImplementedException();
 16: 		}
 17: 
 18: 		public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
 19: 		{
 20: 			string controller = String.Empty;
 21: 			string action = String.Empty;
 22: 
 23: 			if (!String.IsNullOrEmpty(viewName))
 24: 			{
 25: 				string[] contentKey = viewName.Split(new char[] { '_' });
 26: 				if (contentKey.Length != 2)
 27: 				{
 28: 					throw new InvalidOperationException();
 29: 				}
 30: 
 31: 				controller = contentKey[0];
 32: 				action = contentKey[1];
 33: 			}
 34: 
 35: 			if (String.IsNullOrEmpty(controller))
 36: 			{
 37: 				controller = controllerContext.RouteData.Values["controller"].ToString();
 38: 			}
 39: 
 40: 			if (String.IsNullOrEmpty(action))
 41: 			{
 42: 				action = controllerContext.RouteData.Values["action"].ToString();
 43: 			}
 44: 			
 45: 			ViewEngineResult result = new ViewEngineResult(
 46: 				new XmlView(
 47: 					controller,
 48: 					action
 49: 				),
 50: 				this
 51: 			);
 52: 
 53: 			return result;
 54: 		}
 55: 
 56: 		public void ReleaseView(ControllerContext controllerContext, IView view)
 57: 		{
 58: 			IDisposable disposable = view as IDisposable;
 59: 			if (disposable != null)
 60: 			{
 61: 				disposable.Dispose();
 62: 			}
 63: 		}
 64: 
 65: 		#endregion
 66: 	}
 67: }
 68: 


예제로 구현하는 XmlViewEngine 클래스는 FindPartialView 메서드는 지원하지 않으며 FindView 메서드만 지원합니다. FindView 메서드를 구현하는 코드는 그다지 어렵지는 않습니다. 간략히 설명드리면 viewName 인수에 전달된 뷰 이름을 언더스코어 문자(“_”)로 구분하여 컨트롤러와 액션 메서드의 이름을 각각 알아냅니다. 만일 값이 비어있다면 ControllerContext 클래스의 RouteData 속성에서 현재 요청에 사용된 컨트롤러와 액션 메서드의 이름을 그대로 사용합니다. 그런 후에는 새로운 ViewEngineResult 클래스의 인스턴스를 생성하며 이 때 XmlView 클래스의 인스턴스를 전달합니다. ReleaseView 메서드는 view 매개 변수에 전달된 IView 객체가 IDisposable 인터페이스를 구현하고 있다면 Dispose 메서드를 호출해 줍니다. XmlView 클래스는 다음과 같이 구현합니다.

코드 5: XmlView 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: using System.IO;
  7: using System.Web.Routing;
  8: using System.Xml.Linq;
  9: using System.Web.Hosting;
 10: 
 11: namespace MvcApplication1.ViewEngine
 12: {
 13: 	public class XmlView : IView
 14: 	{
 15: 		private string contentKey = String.Empty;
 16: 		private string requestMethod = String.Empty;
 17: 
 18: 		public XmlView(string controller, string action)
 19: 		{
 20: 			this.contentKey = String.Format(
 21: 				"{0}_{1}",
 22: 				controller,
 23: 				action
 24: 			);
 25: 
 26: 			requestMethod = HttpContext.Current.Request.HttpMethod;
 27: 		}
 28: 
 29: 		public XmlView(string controller, string action, string method)
 30: 			: this(controller, action)
 31: 		{
 32: 			requestMethod = method;
 33: 		}
 34: 
 35: 		private string GetContents(string contentKey, string httpMethod)
 36: 		{
 37: 			string contentFilePath = HostingEnvironment.MapPath("~/App_Data/Contents.xml");
 38: 			if (!File.Exists(contentFilePath)) {
 39: 				throw new FileNotFoundException();
 40: 			}
 41: 
 42: 			XDocument contents = XDocument.Load(contentFilePath);
 43: 
 44: 			var selectedContent = from content in contents.Descendants("content")
 45: 								  where content.Element("key").Value == this.contentKey
 46: 								  &&
 47: 								  (
 48: 									(content.Element("method").Value == String.Empty ||
 49: 									content.Element("method").Value == this.requestMethod)
 50: 								  )
 51: 								  orderby content.Element("method").Value descending
 52: 								  select content;
 53: 
 54: 			if (selectedContent == null || selectedContent.Count() == 0)
 55: 			{
 56: 				return String.Empty;
 57: 			}
 58: 			else
 59: 			{
 60: 				return selectedContent.First<XElement>().Element("body").Value;
 61: 			}
 62: 		}
 63: 
 64: 		#region IView Members
 65: 
 66: 		public void Render(ViewContext viewContext, TextWriter writer)
 67: 		{
 68: 			string content = this.GetContents(this.contentKey, this.requestMethod);
 69: 			writer.Write(content);
 70: 		}
 71: 
 72: 		#endregion
 73: 	}
 74: }
 75: 


XmlView 클래스는 GetContents 메서드와 Render 메서드를 가지고 있습니다. 이 중 Render 메서드는 IView 인터페이스에 정의된 메서드이며 GetContents 메서드를 호출하여 자신이 렌더링할 HTML 마크업 코드를 가져옵니다. 따라서 GetContents 메서드가 이 클래스의 핵심이며 코드에서 알 수 있듯이 Contents.xml 파일을 읽은 후 LINQ to XML을 이용하여 지정된 이름의 (컨트롤러_액션 형식의) 콘텐츠를 가져와 가장 첫 번째 요소의 <body> 요소 내의 문자열을 리턴하게 됩니다.

이렇게 해서 XmlViewEngine 클래스와 XmlView 객체를 완성했습니다. 이제 Controller 클래스가 XML 파일 내의 콘텐츠를 렌더링할 수 있도록 XmlView()라는 이름의 메서드를 구현해 보겠습니다. C#의 확장 메서드(Extension Method)를 이용하면 다음과 같이 ControllerExtensions 클래스를 구현해 보겠습니다.

코드 6: ControllerExtensions 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1.ViewEngine
  8: {
  9: 	public static class ControllerExtensions
 10: 	{
 11: 		public static XmlViewResult XmlView(this Controller controller)
 12: 		{
 13: 			return XmlView(controller, null, null);
 14: 		}
 15: 
 16: 		public static XmlViewResult XmlView(this Controller controller, string actionName)
 17: 		{
 18: 			return XmlView(controller, actionName, null);
 19: 		}
 20: 
 21: 		public static XmlViewResult XmlView(this Controller controller, string actionName, string controllerName)
 22: 		{
 23: 			if (String.IsNullOrEmpty(actionName))
 24: 			{
 25: 				actionName = controller.ControllerContext.RouteData.Values["action"].ToString();
 26: 			}
 27: 
 28: 			if (String.IsNullOrEmpty(controllerName))
 29: 			{
 30: 				controllerName = controller.ControllerContext.RouteData.Values["controller"].ToString();
 31: 			}
 32: 
 33: 			XmlViewResult result = new XmlViewResult();
 34: 			result.Controller = controllerName;
 35: 			result.Action = actionName;
 36: 
 37: 			return result;
 38: 		}
 39: 	}
 40: }
 41: 


코드에서 보듯이 ControllerExtensions 클래스는 총 세 개의 XmlView라는 확장 메서드를 정의하고 있습니다. 이 메서드들은 Controller 클래스 및 이를 상속하는 클래스로 확장되며 XmlViewResult 클래스의 인스턴스를 생성하여 리턴합니다. XmlViewResult 클래스는 컨트롤러의 액션 메서드가 리턴하는 ActionResult 클래스를 상속한 클래스로 다음과 같이 구현합니다.

코드 7: XmlViewResult 클래스의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: 
  7: namespace MvcApplication1.ViewEngine
  8: {
  9: 	public class XmlViewResult : ActionResult
 10: 	{
 11: 		public XmlViewResult()
 12: 		{
 13: 		}
 14: 
 15: 		public IView View { get; set; }
 16: 		public string Controller { get; set; }
 17: 		public string Action { get; set; }
 18: 
 19: 		protected ViewEngineResult FindView(ControllerContext context)
 20: 		{
 21: 			if (String.IsNullOrEmpty(this.Controller)) {
 22: 				this.Controller = context.RouteData.Values["controller"].ToString();
 23: 			}
 24: 
 25: 			if (String.IsNullOrEmpty(this.Action)) {
 26: 				this.Action = context.RouteData.Values["action"].ToString();
 27: 			}
 28: 
 29: 			string viewName = String.Format("{0}_{1}", this.Controller, this.Action);
 30: 			ViewEngineResult result = ViewEngines.Engines.FindView(context, viewName, null);
 31: 
 32: 			if (result.View != null)
 33: 			{
 34: 				return result;
 35: 			}
 36: 			throw new InvalidOperationException();
 37: 		}
 38: 
 39: 		public override void ExecuteResult(ControllerContext context)
 40: 		{
 41: 			ViewEngineResult result = null;
 42: 			if (this.View == null)
 43: 			{
 44: 				result = this.FindView(context);
 45: 				this.View = result.View;
 46: 			}
 47: 
 48: 			ViewContext viewContext = new ViewContext(context, this.View, new ViewDataDictionary(), new TempDataDictionary());
 49: 			this.View.Render(viewContext, context.HttpContext.Response.Output);
 50: 			if (result != null)
 51: 			{
 52: 				result.ViewEngine.ReleaseView(context, this.View);
 53: 			}
 54: 		}
 55: 	}
 56: }
 57: 


XmlViewResult 클래스는 ActionResult 클래스에 정의된 ExecuteResult 메서드를 구현해야 합니다. 이 메서드는 액션 메서드가 리턴한 ActionResult 클래스가 실행될 때 호출되며 XmlViewResult 클래스는 자신의 View 속성에 IView 인터페이스 객체가 존재하지 않으면 FindView 메서드를 호출하여 뷰를 탐색하고 탐색된 뷰의 Render 메서드를 호출합니다. FindView 메서드는 ViewEngines.Engines 컬렉션의 FindView 메서드를 호출하여 리턴된 ViewEngineResult 클래스의 View 속성이 null이 아니면 이를 다시 리턴하게 됩니다.

이로서 XmlViewEngine과 관련된 클래스의 구현을 모두 마쳤습니다. 이제 ASP.NET MVC 프레임워크가 우리가 만든 XmlViewEngine 클래스를 사용할 수 있도록 XmlViewEngine 클래스를 등록해 주어야 합니다. Global.asax.cs 파일을 열고 아래와 같이 코드를 작성합니다.

코드 8: Global.asax.cs 파일의 코드

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Web;
  5: using System.Web.Mvc;
  6: using System.Web.Routing;
  7: using MvcApplication1.ViewEngine;
  8: 
  9: namespace MvcApplication1
 10: {
 11: 	// Note: For instructions on enabling IIS6 or IIS7 classic mode, 
 12: 	// visit http://go.microsoft.com/?LinkId=9394801
 13: 
 14: 	public class MvcApplication : System.Web.HttpApplication
 15: 	{
 16: 		public static void RegisterRoutes(RouteCollection routes)
 17: 		{
 18: 			routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 19: 
 20: 			routes.MapRoute(
 21: 				"Default",                                              // Route name
 22: 				"{controller}/{action}/{id}",                           // URL with parameters
 23: 				new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
 24: 			);
 25: 
 26: 		}
 27: 
 28: 		public static void RegisterViewEngine()
 29: 		{
 30: 			ViewEngines.Engines.Add(new XmlViewEngine());
 31: 		}
 32: 
 33: 		protected void Application_Start()
 34: 		{
 35: 			RegisterRoutes(RouteTable.Routes);
 36: 			RegisterViewEngine();
 37: 		}
 38: 	}
 39: }


이로써 필요한 모든 준비를 마쳤습니다.이제 프로젝트를 빌드하고 실행해보면 Home/Index URL을 호출했을 때 아래 그림과 같은 페이지가 나타나는 것을 볼 수 있습니다.

그림 3: Home/Index 페이지의 모습
 img3

여기서 [About] 링크를 클릭해보면 아래 그림과 같이 Home/About 페이지의 GET 형식 콘텐츠가 나타납니다.

그림 4: Home/About 페이지의 GET 요청에 대한 모습
 img4

여기서 다시 [Submit] 버튼을 클릭해보면 이번에는 Home/About 페이지의 POST 형식 콘텐츠가 나타나는 것을 볼 수 있습니다.

그림 5: Home/About 페이지의 POST 요청에 대한 모습
 img5

이상으로 간단한 사용자 정의 ViewEngiine의 구현에 대해 알아보았습니다.

Conculsion

다소 급하게 작성한 글이라 설명이 여러 모로 부족한 것을 느끼지만 예제 코드 자체가 그다지 어렵지 않으므로 이해하시기에는 무리가 없으리라 판단됩니다. 이번 포스트를 통해 살펴보았듯이 ASP.NET MVC의 가장 큰 장점 중 하나는 ASP.NET MVC 프레임워크 자체가 다양한 형태로 확장이 가능하도록 구현되어져 있으며 이를 이용하면 ASP.NET MVC 프레임워크의 세밀한 동작을 개발자가 마음대로 제어할 수 있다는 점입니다. 이런 장점은 HTTP 프로토콜에 대한 추상화 덕택에 오히려 확장성 면에서는 다소 떨어지는 모습을 보이는 ASP.NET MVC 웹 폼 모델과 비교해보면 더욱 크게 느껴질 수도 있습니다.

다음 포스트에도 ASP.NET MVC의 여러 가지 재미있는 기능들을 소개해 드릴 수 있게 되기를 기대하며 이번 포스트를 마칩니다.

Posted by 웹지니 트랙백 0 : 댓글 2