Freigeben über


Packaging UI Components in MVC

Something I get asked by most customers starting out with ASP.NET MVC is how they should package, group, factor, and reuse their UI components. We're all used to thinking about User Controls, Custom Controls, and other Web Forms approaches. But rest assured, MVC provides a wealth of options.

The thoughts below describe how I see each of these options being put to best use. That doesn't mean the guidelines below are completely concrete (or indeed complete) – but they should get you thinking along the right lines initially.

If you've thoughts to add please do shout up!

Custom Rendering Extensions

Rendering extensions are Extension Methods applied to the HtmlHelper class so that they can be used in views to output clean HTML. They are best used for packaging reusable pieces of HTML found across a site. Often they're reusable across multiple web sites. To summarise they;

· Output simple HTML

· Focus on small portions of HTML

· Do not usually contain or use business logic

· Do not contain layout for entities

· Are often reusable across multiple web sites

A good example might be a rendering extension to output a textbox and matching label, or perhaps a button with some specific attributes set.

Custom Action Filters

I do not see Action Filters as a form of UI composition on their own (although the p&p guys did come up with an interesting way of using them in combination with Partial Views). However, they can be really useful for applying behaviour that has a UI focus.

A great example of this is the ShowNothingIfNotAuthorizedAttribute class in the Web Client Guidance RI. It looks like this;

public class ShowNothingIfNotAuthorizedAttribute :

    AuthorizeAttribute

{

    protected override void HandleUnauthorizedRequest(

        AuthorizationContext filterContext)

    {

        filterContext.Result = new EmptyResult();

    }

}

I think this is pretty self explanatory – if the user isn't authorized, they see no content. And this works if the current view is included in a parent view using RenderAction. I would not suggest that you use this approach to build HTML though; it should be reusable behaviour applied to multiple actions, that is agnostic of the content being generated. In summary they;

· Do not output HTML without delegating to a view

· May reference some business logic, but be careful applying this to many views (it can lead to thousands of unintended database hits)

· Do not contain layout

· Are often reusable across multiple web sites

Html.RenderPartial

Partial Views and Html.RenderPartial should be used for creating HTML content that meets the following criteria;

· The HTML is more complex or larger than makes sense for a rendering extension (the visual editing of a partial view is a great help for maintenance purposes here)

· It is reused across the web site

· It does not contain reused business logic – just layout

· It does not contain layout for entities

· It may contain other composition techniques – like templates or Html.RenderAction.

A good example is perhaps defining how a list of some form should be displayed. The list and individual items themselves would probably use templates (see below), but the surrounding layout and chrome would fit well in a partial view.

In truth, the dividing line between a partial view and RenderAction is more about the design of your controller action. If it passes all the data the partial view needs, and there is no need to split out behaviour for fetching that data, a partial view makes sense.

Custom Display and Editor Templates

Templates are new in MVC 2 and allow you to use syntax such as the following;

<%= Html.DisplayFor(m => m.Artist)%>

This outputs HTML according to the type of the argument. This type could be simple (e.g. string, integer, etc), or it could be a business-specific entity (e.g. Song, User, ShoppingCart, and so on). They can be customised by dropping partial views into the DisplayTemplates or EditorTemplates subfolders for a controller.

This means these basically follow the same rules as for partial views, except that they do contain layout for entities (note: I mean entities specific to your web site here; call them what you like... domain entities, view models, business entities, etc, depending upon your architecture).

· The HTML is more complex or larger than makes sense for a rendering extension

· It is reused across the web site

· It does not contain reused business logic – just layout

· It does contain layout for a specific breed of entity or type

· It may make some limited use of other composition techniques

Html.RenderAction

Using RenderAction is different to RenderPartial in that it executes a controller action and then inserts the HTML output into the parent view. This means there is one very strong distinction between the two approaches – RenderAction should be used when behaviour as well as layout must be reused. In summary, use it when;

· HTML is more complex or larger than makes sense for a rendering extension

· It does contain both reused business logic / behaviour and layout

· It does not contain layout for entities

· It may make use of other composition techniques

A good example of this might be rendering a shopping basket. You do not want all the actions responsible for views that include a shopping basket to have to retrieve the basket content, pass it in the view model, and use RenderPartial to pass it to the basket partial view. Instead, it makes much more sense to call out from the host view to a ShoppingBasket controller's Display action. This also makes it much easier if the content should be rendered using a different view depending upon business logic – perhaps a ShoppingCart.aspx view or a NotLoggingIn.aspx view.

RenderAction is also handy when composing views across modules. This is called out in the docs for the Web Client Guidance (check the "UI Composition" topic). An example of this in action is the incorporation of "My Friends Top Songs" in the home page for the Reference Implementation using this syntax;

<% Html.RenderAction("FriendsTopSongs", "TopSongs",

   new {area = "TopSongs"});%>

The logic for what to render in this list and the data access to get the list is found at target route.

UI Extensions

Finally, patterns & practices came up with an interesting way of making part of a view extensible – and that is UI Extensions. The idea here is that a view defines a location that can be extended by other components, probably in different modules. It does this by iterating through a collection of extensions and rendering them with code something like the following;

<div class="options">

    <% foreach (SongLinkMetadata songAction in song.SongActions)

       {

           Html.RenderPartial("SongAction", songAction);

       } %>

</div>

This is taken from "~/Views/Search/Results.aspx" in the Web Client Guidance Reference Implementation. The MyLibrary module then registers a type called AddToLibrarySongActionProvider that populates this region. In this case,

· The HTML layout is fixed by the extension point, not the extensions

· The logic to provide these extensions exists in decoupled code, or another module

Summary

Well this post is way longer than I wanted it to be, but I hope it explains the basics of your options. The aim is really to introduce the concepts, and then to encourage you to investigate the Web Client Guidance to learn more and see these approaches in action.

Do remember what I said – these are not hard and fast rules, but they should get you started. I have also avoided the debate about what does and does not adhere to the MVC pattern – for example RenderAction is seen by some as breaking the rules. Make your own mind up!