25

In Razor, there's a curious rule about only allowing closed HTML within an if block.

See:

Razor doesn't understand unclosed html tags

But I have a situation where I want to exclude some outer, wrapping elements under certain conditions. I don't want to repeat all the inner HTML, which is a fair amount of HTML and logic.

Is the only way around the problem to make yet another partial view for the inner stuff to keep it DRY?

Without any other re-use for this new partial, it feels really awkward, bloaty. I wonder if the rule is a limitation of Razor or simply a nannying (annoying) feature.

Community
  • 1
  • 1
Luke Puplett
  • 42,091
  • 47
  • 181
  • 266

4 Answers4

43

You can use Html.Raw(mystring). In myString you can write whatever you want, for example a tag opening or closing, without getting any errors at all. I.e.

if (condition) {
  @Html.Raw("<div>")
}

if (condition) {
  @Html.Raw("</div>")
}

NOTE: the @Html.Raw("<div>") can be written in a shorter, alternative form, like this: @:<div>

You can also create your own html helpers for simplifying the razor syntax. These helpers could receive the condition parameter, so that you can do something like this:

@Html.DivOpen(condition)

@Html.DivClose(condition)

or more complicated helpers that allow to specify attributes, tag name, and so on.

Nor the Raw, neither the html helpers will be detected as "tags" so you can use them freely.

The Best Way to do it

It would be much safer to implement something like the BeginForm html helper. You can look at the source code. It's easy to implement: you simply have to write the opening tag in the constructor, and the closing tag in the Dispose method. The adavantage of this technique is that you will not forget to close a conditionally opened tag.

You need to implement this html helper (an extension method declared in a static class):

  public static ConditionalDiv BeginConditionalDiv(this HtmlHelper html, 
    bool condition)
  {
      ConditionalDiv cd = new ConditionalDiv(html, condition);
      if (condition) { cd.WriteStart(); }
      return cd; // The disposing will conditionally call the WriteEnd()
  }

Which uses a class like this:

  public class ConditionalDiv : IDisposable
  {
      private HtmlHelper Html;
      private bool _disposed;
      private TagBuilder Div;
      private bool Condition;

      public ConditionalDiv(HtmlHelper html, bool condition)
      {
          Html = html;
          Condition = condition;
          Div = new TagBuilder("div");
      }

      public void Dispose()
      {
          Dispose(true /* disposing */);
          GC.SuppressFinalize(this);
      }

      protected virtual void Dispose(bool disposing)
      {
        if (!_disposed)
        {
            _disposed = true;
            if (Condition) { WriteEnd(); }
        }
      }

       public void WriteStart()
       {
           Html.ViewContext.Writer.Write(Div.ToString(TagRenderMode.StartTag));
       }

       private void WriteEnd()
       {
           Html.ViewContext.Writer.Write(Div.ToString(TagRenderMode.EndTag));
       }
  }

You can use this with the same pattern as BeginForm. (Disclaimer: this code is not fully tested, but gives an idea of how it works. You can accept extra parameters for attributes, tag names and so on).

ajbeaven
  • 9,265
  • 13
  • 76
  • 121
JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • Thanks - the question is really to see what strategies for this people are using out in the wild. – Luke Puplett Jun 24 '14 at 11:30
  • Woah. That's a pretty admirable answer. – Luke Puplett Jun 24 '14 at 11:56
  • Thanks. Helped me and I think @:
    is better because it don't need to change " to ' or somethings other if HTML was complex. see http://stackoverflow.com/questions/11969070/mvc3-razor-conditional-wrapper-div
    – QMaster Jan 06 '15 at 15:08
  • The `BeginConditionalDiv` extension method can also return `null` (or a no-op `IDisposable` class) instead, so you don't need to pass the condition into the `ConditionalDiv` class and have additional code to write the conditional start/end tag. – Ronald Dec 02 '19 at 10:51
  • @Ronald I don't understand what you mean. Can you explain it? – JotaBe Dec 02 '19 at 17:11
  • The helper method can be simplified to `return condition ? new ConditionalDiv(html, condition) : null;` if the `ConditionalDiv` writes the start tag in the constructor and end tag in the dispose method. – Ronald Dec 06 '19 at 13:11
  • My sample follows the same pattern of `BeginForm`. It doesn't make any difference in the Razor usage. Apart from this, for me it's a smell to do actions in constructors: they should only be used to initialize objects. – JotaBe Dec 10 '19 at 10:11
7

Declare razor helper using @helper HeplerName(), call by @HeplerName() anywhere. And you can add params if you want.

@if (condition)
{
  <div class="wrapper">
    @MyContent()
  </div>
}
else
{
  @MyContent()
}

@helper MyContent()
{
  <img src="/img1.jpg" />
}

Edit (thanks to @Kolazomai for bringing up the point in comments):

ASP.NET Core 3.0 no longer supports @helper but supports HTML markup in method body. New case will look like this:

@if (condition)
{
  <div class="wrapper">
    @{ MyContent(); }
  </div>
}
else
{
  @{ MyContent(); }
}

@{
  void MyContent()
  {
    <img src="/img1.jpg" />
  }
}
Ivan Maslov
  • 168
  • 2
  • 13
1

The following code is based on the answer of @JotaBe, but simplified and extendable:

public static class HtmlHelperExtensions
{
    public static IDisposable BeginTag(this HtmlHelper htmlHelper, string tagName, bool condition = true, object htmlAttributes = null)
    {
        return condition ? new DisposableTagBuilder(tagName, htmlHelper.ViewContext, htmlAttributes) : null;
    }
}

public class DisposableTagBuilder : TagBuilder, IDisposable
{
    protected readonly ViewContext viewContext;
    private bool disposed;

    public DisposableTagBuilder(string tagName, ViewContext viewContext, object htmlAttributes = null) : base(tagName)
    {
        this.viewContext = viewContext;

        if (htmlAttributes != null)
        {
            this.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
        }

        this.Begin();
    }

    protected virtual void Begin()
    {
        this.viewContext.Writer.Write(this.ToString(TagRenderMode.StartTag));
    }

    protected virtual void End()
    {
        this.viewContext.Writer.Write(this.ToString(TagRenderMode.EndTag));
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            this.disposed = true;

            this.End();
        }
    }
}

This can be used the following way within Razor views:

@using (Html.BeginTag("div"))
{
    <p>This paragraph is rendered within a div</p>
}

@using (Html.BeginTag("div", false))
{
    <p>This paragraph is rendered without the div</p>
}

@using (Html.BeginTag("a", htmlAttributes: new { href = "#", @class = "button" }))
{
    <span>And a lot more is possible!</span>
}
Ronald
  • 1,795
  • 14
  • 17
  • This worked very good. Thank you. I tried to convert it to ASP.NET Core MVC without luck. It appears the this.ToString(TagRenderMode) overload is missing on .NET Core. – vhugo Nov 05 '20 at 19:33
0

This is IMO the most convenient way to achieve this:

[HtmlTargetElement(Attributes = RenderAttributeName)]
public class RenderTagHelper : TagHelper
{
    private const string RenderAttributeName = "render";
    private const string IncludeContentAttributeName = "include-content";

    [HtmlAttributeName(RenderAttributeName)]
    public bool Render { get; set; }

    [HtmlAttributeName(IncludeContentAttributeName)]
    public bool IncludeContent { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (!Render)
        {
            if (IncludeContent)
                output.SuppressOutput();
            else
                output.TagName = null;
        }

        output.Attributes.RemoveAll(RenderAttributeName);
        output.Attributes.RemoveAll(IncludeContentAttributeName);
    }
} 

Then, you just use it like this:

<div render="[bool-value]" include-content="[bool-value]">
    ...
</div>
Javid
  • 2,755
  • 2
  • 33
  • 60