How to abstract common snippets of markup with ASP

2020-07-18 03:08发布

问题:

I have a lot of content-heavy views in my ASP.NET MVC 2 site. These contain several re-occurring HTML patterns. When using ASP.NET Webforms, a class derived from WebControl could encapsulate these patterns. I'd like some pointers on the correct approach for this problem with MVC.

Detailed Explanation

Patterns not unlike the following HTML markup keep occurring throughout these views. The markup renders into an isolated a box of content:

<div class="top container">
    <div class="header">
       <p>The title</p>
       <em>(and a small note)</em>
    </div>
    <div class="simpleBox rounded">
       <p>This is content.</p>
       <p><strong>Some more content</strong></p>
    </div>
</div>

This is a trivial example, but there are more complex recurring patterns. In ASP.NET Webforms I would have abstracted such code into a WebControl (let's say I'd have named it BoxControl), being included on a page like this:

<foo:BoxControl runat="server">
  <Header>The title</Header>
  <Note>(and a small note)</Note>
  <Content>
     <p>This is content.</p>
     <p><strong>Some more content</strong></p>
  </Content>
</foo:BoxControl>

This abstraction makes it easy to adapt the way the box is constructed throughout the site, by just altering the BoxControl source. It also keeps the static HTML content neatly together in the View Page, even when combining several BoxControls on a page. Another benefit is that the HTML used as content is recognized by the IDE, thus providing syntax highlighting/checking.

To my understanding, WebControls are discouraged in ASP.NET MVC. Instead of a WebControl, I could accomplish the abstraction with a partial view. Such a view would then be included in a View Page as follows:

<%= Html.Partial("BoxControl", new {
  Header="The Title", 
  Note="(and a small note)", 
  Content="<p>This is content.</p><p><strong>Some more content</strong></p>"});
%>

This is not ideal, since the 'Content' parameter could become very long, and the IDE does not treat it as HTML when passed this way.

Considered Solutions Strongly-Typed ViewModels can be passed to the Html.Partial call instead of the lengthy parameters shown above. But then I'd have to pull the content in from somewhere else (a CMS, or Resource file). I'd like for the content to be contained in the View Page.

I have also considered the solution proposed by Jeffrey Palermo, but that would mean lots of extra files scattered around the project. I'd like the textual content of any view to be restricted to one file only.

Should I not want to abstract the markup away? Or is there maybe an approach, suitable for MVC, that I am overlooking here? What is the drawback to 'sinning' by using a WebControl?

回答1:

There is a solution to this problem, although the way to get there is a little more clutsy than other frameworks like Ruby on Rails.

I've used this method to create markup for Twitter Bootstrap's control group syntax which looks like this:

<div class="control-group">
  <label class="control-label">[Label text here]</label>
  <div class="controls">
    [Arbitrary markup here]
  </div>
</div>

Here's how:

1) Create a model for the common markup snippet. The model should write markup on construction and again on dispose:

using System;
using System.Web.Mvc;

namespace My.Name.Space
{
  public class ControlGroup : IDisposable
  {
    private readonly ViewContext m_viewContext;
    private readonly TagBuilder m_controlGroup;
    private readonly TagBuilder m_controlsDiv;

    public ControlGroup(ViewContext viewContext, string labelText)
    {
      m_viewContext = viewContext;
      /*
       * <div class="control-group">
       *  <label class="control-label">Label</label>
       *  <div class="controls">
       *   input(s)
       *  </div>
       * </div>
       */

      m_controlGroup = new TagBuilder("div");
      m_controlGroup.AddCssClass("control-group");
      m_viewContext.Writer.Write(m_controlGroup.ToString(TagRenderMode.StartTag));

      if (labelText != null)
      {
        var label = new TagBuilder("label");
        label.AddCssClass("control-label");
        label.InnerHtml = labelText;
        m_viewContext.Writer.Write(label.ToString());
      }

      m_controlsDiv = new TagBuilder("div");
      m_controlsDiv.AddCssClass("controls");
      m_viewContext.Writer.Write(m_controlsDiv.ToString(TagRenderMode.StartTag));

    }

    public void Dispose()
    {
      m_viewContext.Writer.Write(m_controlsDiv.ToString(TagRenderMode.EndTag));
      m_viewContext.Writer.Write(m_controlGroup.ToString(TagRenderMode.EndTag));
    }
  }
}

2) Create a nifty Html helper

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

using My.Name.Space

namespace Some.Name.Space
{
  public static class FormsHelper
  {
    public static ControlGroup ControlGroup(this HtmlHelper helper, string labelText)
    {
      return new ControlGroup(helper.ViewContext, labelText);
    }
  }
}

3) Use it in the view (Razor code)

@using (Html.ControlGroup("My label"))
{
  <input type="text" />
  <p>Arbitrary markup</p>
  <input type="text" name="moreInputFields" />
}

This is also the way MVC framework renders a form with the Html.BeginForm method



回答2:

Well you wouldn't render the partial like that, pass it a strongly-typed ViewModel, like this:

<%= Html.RenderPartial("BoxControl", contentModel) %>

contentModel is the ViewModel (just a POCO-like storage mechanism for your views), which the strongly typed partial view would bind to.

So you can do this in your partial view:

<h1><%: Model.Header %></h1>
<p><%: Model.Content %></p>

etc etc



回答3:

After considering the answers and running an experiment, I'm inclined to adhere to the pure MVC approach and duplicate some presentation code throughout View Pages. I'd like to elaborate on the rationale for that decision.

Partial View When using a Partial View, The content for the box needs to be passed as a View Model, making the View Page less readable versus declaring the content HTML on the spot. Remember that the content does not come from a CMS, so that would mean filling the View Model with HTML in a controller or setting a local variable in the View Page. Both of these methods fail to take advantage of IDE features for dealing with HTML.

WebControl On the other hand, a WebControl-derived class is discouraged and also turns out to have some practical issues. The main issue that the declarative, hierarchical style of traditional ASP.NET .aspx pages just does not fit the procedural style of MVC.NET View Pages. You have to choose for either a full blown traditional approach, or go completely MVC.

To illustrate this, the most prominent issue in my experimental implementation was one of variable scope: when iterating a list of products, the MVC-way is to use a foreach loop, but that introduces a local variable which will not be available in the scope of the WebControl. The traditional ASP.NET approach would be to use a Repeater instead of the foreach. It seems to be a slippery slope to use any traditional ASP.NET controls at all, because I suspect you'll soon find yourself needing to combine more and more of them to get the job done.

Plain HTML Forgoing the abstraction at all, you are left with duplicate presentation code. This is against DRY, but produces very readable code.



回答4:

It doesnt look like webforms has that much less html to me, it seems more like a lateral move. Using a partial in MVC can make it cleaner but the html markup you needed will still be there, in one place or another. If its mostly the extra html that bothers you, you might take a look at the NHaml view engine or check out haml the haml website.

I'm by no means a Haml expert but the html does look a lot cleaner...

.top container
    .header
       %p 
         The title
       %em 
         (and a small note)
    .simpleBox rounded
       %p 
         This is content.
       %p 
         Some more content