How to abstract common snippets of markup with ASP.NET MVC
Asked Answered
L

4

4

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?

Lefkowitz answered 2/11, 2010 at 23:56 Comment(1)
I re-read your question and I think @RPM1984 is probably the best way to goRecurvate
L
1

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.

Lefkowitz answered 15/11, 2010 at 17:2 Comment(0)
E
4

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

Ecstasy answered 22/1, 2013 at 9:21 Comment(0)
P
1

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

Phineas answered 3/11, 2010 at 0:3 Comment(4)
Thanks for your answer, it is indeed an alternative to what I considered. However, I am looking for a way that keeps the actual (fairly static) content in the View Page. I have edited the question to clarify this.Lefkowitz
i'm not sure what you mean why "keeps the actual (fairly static) content in the View Page." whats the point of the partial/user control then? what information/logic/code is being re-used?Phineas
I'd like the View Page to contain organized content without presentation-markup. Presentation ideally comes from external CSS files, but for complex layouts CSS files alone don't always suffice and some ugly non-content (presentation) HTML is needed. This is the case with a complex 'box' layout I use frequently throughout my View Pages. I'd like to abstract that presentation-HTML away, so that when I decide to change the appearance of the box, i don't have to go through lots of Views to change the markup. To answer your question: I want to re-use ugly recurring non-content presentation HTML.Lefkowitz
@Graham Sunderland - right ok, so you basically want a "User Control" (web forms talk), with actual HTML, and no model binding/logic. You can still do that with a Partial View, just make it <dynamic>, not bound to a particular model. As for the "Content" parameter, you could put that in ViewData (set it in the parent view, access it in the partial view). Would that work?Phineas
L
1

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.

Lefkowitz answered 15/11, 2010 at 17:2 Comment(0)
R
0

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
Recurvate answered 3/11, 2010 at 0:16 Comment(1)
thanks Justin, an interesting approach. haml only compresses the markup though, it's not abstracting it away to someplace else. It doesn't matter to me if it's more or less code, only whether maintenance and readability are improved.Lefkowitz

© 2022 - 2024 — McMap. All rights reserved.