블로그 이미지
Sunny's

calendar

1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

Notice

2010. 7. 5. 21:00 ASP.NET

The ASP.NET MVC team just dropped CodePlex Preview 5. It contains a whole raft of new stuff, but there are two highly related features that I worked on. Since I have one foot in the "Dynamic Data" space and one foot into the "MVC" space (being the guy working on "Dynamic Data for MVC"), it means I get to have conversations like this:

Me: I need partial rendering for Dynamic Data for MVC.
Phil Haack: Yeah, okay, I'll put it on a list somewhere.

(...time passes...)

Me: I need partial rendering, like, soon!
Phil: I know! Go away.

(...time passes...)

Me: Okay, seriously? I need partial rendering tomorrow.
Phil: Sounds like you'll be doing it yourself then, right? ... Right?
Me: ...

I totally walked into that.

The team went through a few iterations of design discussions with me spiking a variety of ways to implement the feature. One thing we wanted to be sure to enable was the idea that the partial view you render can be done with a different view engine if necessary.

In the process, we uncovered several issues with the current implementation of view engines.

  • Controllers choose the view engine. Since the base Controller class chooses the WebForms view engine by default, many MVC users might not even realize this. Does the decision about view engines really belong with the controllers? They are otherwise divorced from view details, so this felt wrong.
  • Installing View Engines was awkward. Since controllers make the decision about view engines, how do you install a view engine? The "least intrusive" method turned out to be overriding the controller factory, yet another smell that things that shouldn't know about each other, did.
  • The View Engine decision was basically "one at a time". Since controllers make the decision about view engines, and most non-WebForms view engines get injected via the controller factory, you're more or less stuck using just a single view engine in your application (unless you want to perform a bunch of view engine overrides in your controllers).
  • Partial views don't have controllers. When a partial view is rendered, there is no associated controller, so who makes the decision about which view engine to use?

Given those limitations, and our desire to support multiple simultaneous view engines, we decided to revamp the view engine system at the same time we implemented partial rendering.

Partial views and Html.RenderPartial

There are four versions of Html.RenderPartial, and they all take a partial view name as their first parameter.

01 // Renders the partial view using the view data and model from the parent view.
02 public void RenderPartial(string partialViewName);
03   
04 // Renders the partial view with an empty view data and the given model.
05 public void RenderPartial(string partialViewName, object model);
06   
07 // Renders the partial view with the given view data.
08 public void RenderPartial(string partialViewName, ViewDataDictionary viewData);
09   
10 // Renders the partial view with a copy of the given view data and the provided model.
11 public void RenderPartial(string partialViewName, object model, ViewDataDictionary viewData);

The purpose of partial views are to be small, reusable pieces of view. Ideally, they will be able to get the data to display from the ViewData of the parent, or through the use of the model that was passed specifically into them. Since partial views are intended to be a partial rendering, there is no concept of a "master view" for partial views.

In the WebForms view engine, both views and partial views can be either .aspx or .ascx files. In practice, we expect that most views will be .aspx files (pages) and most partial views will be .ascx files (user controls).

Individual view engines can set their own rules for how they process views vs. partial views.

One of the best features of this new system is that your partial views can use a different view engine than your views, and it doesn’t require any coding gymnastics to make it happen. It all comes down to how the new view system resolves which view engine renders which views.

Implementing views: the IView interface

That's right: we brought back IView! You'll notice that RenderPartial returns void, not string. This means you use the inline code syntax (no equal sign):

1 <% Html.RenderPartial("viewName"); %>

When views and partial views render themselves, they are not turned into strings. We did this primarily for reasons of memory consumption and scalability, but also because it was more natural with the way WebForms already work.

There is a single interface which represents both views and partial views:

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

Views are asked to render themselves into a text writer. In production usage, the provided text writer will be HttpContext.Current.Response.Output.

By providing the text writer to the Render method, it means those who are interested in testing the rendering of their views can do so without needing to mock the HttpResponse object. In your tests, you can pass a StringWriter, which wraps around StringBuilder, and inspect the resulting string when you’re done rendering.

The rules of rendering (or, what's that TextWriter there for?)

Consider the following view:

1 <p>Paragraph 1</p>
2 <% Html.RenderView("partialView"); %>
3 <p>Paragraph 3</p>

And the following partial view:

1 <p>Paragraph 2</p>

Let's assume that our view engine renders by putting everything in a string and then doing one big write at the end (there is at least one view engine today which does this, but I don't want to out anybody :-p). Our hypothetical view ends up resulting in the following code:

1 public void Render(ViewContext viewContext, TextWriter writer) {
2     string result = "";
3     result += "<p>Paragraph 1</p>";
4     Html.RenderPartial("partialView");
5     result += "<p>Paragraph 3</p>";
6     viewContext.HttpContext.Response.Write(result);
7 }

And our partial view's code ends up being:

1 public void Render(ViewContext viewContext, TextWriter writer) {
2     string result = "";
3     result += "<p>Paragraph 2</p>";
4     viewContext.HttpContext.Response.Write(result);
5 }

What's the actual output result?

1 <p>Paragraph 2</p>
2 <p>Paragraph 1</p>
3 <p>Paragraph 3</p>

I'm pretty sure that's not what we wanted. :)

Let's consider an implementation that uses the text writer:

1 public void Render(ViewContext viewContext, TextWriter writer) {
2     writer.Write("<p>Paragraph 1</p>");
3     Html.RenderPartial("partialView");
4     writer.Write("<p>Paragraph 3</p>");
5 }

and:

1 public void Render(ViewContext viewContext, TextWriter writer) {
2     writer.Write("<p>Paragraph 2</p>");
3 }

And the new result?

1 <p>Paragraph 1</p>
2 <p>Paragraph 2</p>
3 <p>Paragraph 3</p>

That's much better.

This is why views are given the text writer. Even if we could ignore memory consumption and scalability (I can hear Thomas groaning at such a suggestion), we needed to choose this implementation for another reason: WebForms already exists. :)

There's a whole bunch of infrastructure already set into place with .aspx/.ascx which means these things will be using Response.Write to put content out into the response stream. We need everything to play together in the same way, so passing Response.Output as the text writer encourages the correct implementation from view engine authors. As a bonus, it also gives us that extra test point. You know how much we love testing. :)

Implementing view engines: the IViewEngine interface

We split the behavior of rendering (IView) from the behavior of locating (IViewEngine):

1 public interface IViewEngine
2 {
3     ViewEngineResult FindPartialView(ControllerContext controllerContext,
4                                      string partialViewName);
5     ViewEngineResult FindView(ControllerContext controllerContext,
6                               string viewName, string masterName);
7 }

View engines are about finding views (and partial views). The ViewEngineResult that is returned offers two different constructors:

1 // Use this constructor when you didn't find an appropriate view
2 public ViewEngineResult(IEnumerable<string> searchedLocations);
3   
4 // Use this constructor when you did find a view
5 public ViewEngineResult(IView view);

The system will run through the list of registered view engines (in order of registration) until it finds one that returns a view that can be rendered. If it never finds an appropriate view, then it throws an exception which lists all the locations that were searched.

For example, the WebForms view engine searches the following virtual paths looking for views and partial views:

~/Views/<controllerName>/<viewName>.aspx
~/Views/<controllerName>/<viewName>.ascx
~/Views/Shared/<viewName>.aspx
~/Views/Shared/<viewName>.ascx

Additionally, when you provide a non-null, non-empty master view name for FindView, it looks in the following locations:

~/Views/<controllerName>/<masterName>.master
~/Views/Shared/<masterName>.master

We anticipate that most of the view engines will be based on files in the file system, so we provided a base class which does almost all the hard work: VirtualPathProviderViewEngine. Implementing a view engine to derive from this class is very simple:

  1. In the constructor, set MasterLocationFormats, ViewLocationFormats, and PartialViewLocationFormats.
  2. Override CreateView() and CreatePartialView() to create the views when they've been found.

That's it. In fact, here's the whole source to WebFormsViewEngine:

01 public class WebFormViewEngine : VirtualPathProviderViewEngine {
02     public WebFormViewEngine() {
03         MasterLocationFormats = new[] {
04             "~/Views/{1}/{0}.master",
05             "~/Views/Shared/{0}.master"
06         };
07         ViewLocationFormats = new[] {
08             "~/Views/{1}/{0}.aspx",
09             "~/Views/{1}/{0}.ascx",
10             "~/Views/Shared/{0}.aspx",
11             "~/Views/Shared/{0}.ascx"
12         };
13         PartialViewLocationFormats = ViewLocationFormats;
14     }
15   
16     protected override IView CreatePartialView(ControllerContext controllerContext,
17                                                string partialPath) {
18         return new WebFormView(partialPath, null);
19     }
20   
21     protected override IView CreateView(ControllerContext controllerContext,
22                                         string viewPath,
23                                         string masterPath) {
24         return new WebFormView(viewPath, masterPath);
25     }
26 }

The VirtualPathProviderViewEngine not only does the work of looking up the view through all the provided locations, it also takes care of caching the results of those lookups. When you're running ASP.NET in debug mode, it does not cache the lookup, so that you can add and remove view files without restarting your development web server; when you're running ASP.NET in production mode, it will cache the lookup results with a sliding timeout of 15 minutes.

Changes to Controller/ViewResult

We removed the ViewEngine property from the Controller base class.

On ViewResult, we added the ability for you to set an IView object directly if you need to directly create the view (rather than letting the view engines get a crack at it). We also added an overload to Controller.View() which takes an IView.

We also added a PartialViewResult class, and Controller.PartialView() methods which return it. You might find this useful to return partial views in response to AJAX partial HTML requests.

Registering the view engine

Once the view engine has been written, you register it with the system during Application_Start in your global.asax:

1 ViewEngines.Engines.Add(new MyViewEngine());

When attempting to render a view or partial view, the system will walk the list of view engines in order until one says "I can render this view". By default, the WebForms view engine is registered for you. As you register additional engines, you'll be able to interleave views that use any of the registered engines.

As mentioned earlier, you can render partials that use a different view engine from the parent view. This should help the case where you want to transition from one view engine to another, to be able to keep both running simultaneously, moving views over one at a time. Hopefully we can poke Phil into re-releasing his IronRuby sample, this time using both view engines to mix Ruby RHTML views with WebForms views. :)

ViewEngines.Engines is not thread-safe, so you should only register new view engines during Application_Start. If you need to be able to register view engines dynamically at runtime, you can create an instance of CompositeViewEngine, initialized with a thread safe collection, and then add and remove your view engines with that thread safe collection. Remember to register your instance of CompositeViewEngine into ViewEngine.Engines (in Application_Start, of course).

출처 : http://bradwilson.typepad.com/blog/2008/08/partial-renderi.html

posted by Sunny's