19

Is there more efficient way to build HTML table than the one I'm trying on right now?

I'm getting an object and it has some list of entities in it. So I need to pass through each of them and build first a cell and then add it to an row and finally adding it in table.

The thing I'm trying on is totally messy, kind of works, but it has too much of redundant code.

public static string CreateNotificationMailMessage(NotificationMailMessage mailMessageObject)
{
    var table = new HtmlTable();
    var mailMessage = new StringBuilder();
    string html;

    if (mailMessageObject.InvalidCompanies.Any())
    {
        HtmlTableRow row;
        HtmlTableCell cell;

        foreach (var invalidCompany in mailMessageObject.InvalidCompanies)
        {
            row = new HtmlTableRow();
            cell = new HtmlTableCell();
            cell.InnerText = invalidCompany.BusinessName;
            row.Cells.Add(cell);
            cell.InnerText = invalidCompany.SwiftBIC;
            row.Cells.Add(cell);
            cell.InnerText = invalidCompany.IBAN;
            row.Cells.Add(cell);
            table.Rows.Add(row);
        }
    }
    using (var sw = new StringWriter())
    {
        table.RenderControl(new HtmlTextWriter(sw));
        html = sw.ToString();
    }

    mailMessage.AppendFormat(html);
    return mailMessage.ToString();
}

At the end I want to return text version of created HTML table. The problem is that I have much more properties than those 3 (BusinessName, SwiftBIC and IBAN) and plus I have one more list of objects inside of mailMessageObject, so the code would be terrible.

Anybody has an idea how to solve this in simpler and cleaner way?

3
  • Did you try to use TagBuilder of System.Web.Mvc for generating HTML? I mean not a MVC-app, just including reference for System.Web.Mvc. Commented Apr 7, 2016 at 12:25
  • ". The problem is that I have much more properties than those 3 (BusinessName, SwiftBIC and IBAN)" possible a use case for reflection and a customTag saying 'include this in the html table'. Or perhaps just XmlSerialize the lists and leave the conversion to a html table as a xslt transform on the viewer? Commented Apr 7, 2016 at 13:15
  • Also I think there are at least two things you seems to be asking ('how do I iterate over a bunch of fields / properties in a bunch of lists') => Reflection, probably & how do I build a HTML table efficiently for a given workset. All the answers seem to be about html so if you question is actually about the iteration maybe a little rewording is necessary. Commented Apr 7, 2016 at 13:18

4 Answers 4

13

As I've recently come to play with creating IDisposable classes, I think this would be both efficient for this specific task, and much easier to read:

Create some very simple classes

    /// <summary>
    /// https://stackoverflow.com/a/36476600/2343
    /// </summary>
    public class Table : IDisposable
    {
        private StringBuilder _sb;

        public Table(StringBuilder sb, string id = "default", string classValue="")
        {
            _sb = sb;
            _sb.Append($"<table id=\"{id}\" class=\"{classValue}\">\n");
        }

        public void Dispose()
        {
            _sb.Append("</table>");
        }

        public Row AddRow()
        {
            return new Row(_sb);
        }

        public Row AddHeaderRow()
        {
            return new Row(_sb, true);
        }

        public void StartTableBody()
        {
            _sb.Append("<tbody>");

        }

        public void EndTableBody()
        {
            _sb.Append("</tbody>");

        }
    }

    public class Row : IDisposable
    {
        private StringBuilder _sb;
        private bool _isHeader;
        public Row(StringBuilder sb, bool isHeader = false)
        {
            _sb = sb;
            _isHeader = isHeader;
            if (_isHeader)
            {
                _sb.Append("<thead>\n");
            }
            _sb.Append("\t<tr>\n");
        }

        public void Dispose()
        {
            _sb.Append("\t</tr>\n");
            if (_isHeader)
            {
                _sb.Append("</thead>\n");
            }
        }

        public void AddCell(string innerText)
        {
            _sb.Append("\t\t<td>\n");
            _sb.Append("\t\t\t"+innerText);
            _sb.Append("\t\t</td>\n");
        }
    }
}

Then you can define your table using:

StringBuilder sb = new StringBuilder();

using (Html.Table table = new Html.Table(sb))
{
    foreach (var invalidCompany in mailMessageObject.InvalidCompanies)
    {
        using (Html.Row row = table.AddRow())
        {
            row.AddCell(invalidCompany.BusinessName);
            row.AddCell(invalidCompany.SwiftBIC);
            row.AddCell(invalidCompany.IBAN);
        }
    }
}

string finishedTable = sb.ToString();
Sign up to request clarification or add additional context in comments.

2 Comments

Awesome idea Steve! I supplemented your class definitions to build it out a little more in my answer.
What about columns
12

I would just like to supplement Steve Harris' answer with a class library that is a little more built out. His answer is a totally elegant solution that made a windows service I was creating not have to reference System.Web for no good reason!

Classes Defined:

  public static class Html
  {
    public class Table : HtmlBase, IDisposable
    {
      public Table(StringBuilder sb, string classAttributes = "", string id = "") : base(sb)
      {
        Append("<table");
        AddOptionalAttributes(classAttributes, id);
      }

      public void StartHead(string classAttributes = "", string id = "")
      {
        Append("<thead");
        AddOptionalAttributes(classAttributes, id);
      }

      public void EndHead()
      {
        Append("</thead>");
      }

      public void StartFoot(string classAttributes = "", string id = "")
      {
        Append("<tfoot");
        AddOptionalAttributes(classAttributes, id);
      }

      public void EndFoot()
      {
        Append("</tfoot>");
      }

      public void StartBody(string classAttributes = "", string id = "")
      {
        Append("<tbody");
        AddOptionalAttributes(classAttributes, id);
      }

      public void EndBody()
      {
        Append("</tbody>");
      }

      public void Dispose()
      {
        Append("</table>");
      }

      public Row AddRow(string classAttributes = "", string id = "")
      {
        return new Row(GetBuilder(), classAttributes, id);
      }
    }

    public class Row : HtmlBase, IDisposable
    {
      public Row(StringBuilder sb, string classAttributes = "", string id = "") : base(sb)
      {
        Append("<tr");
        AddOptionalAttributes(classAttributes, id);
      }
      public void Dispose()
      {
        Append("</tr>");
      }
      public void AddCell(string innerText, string classAttributes = "", string id = "", string colSpan = "")
      {
        Append("<td");
        AddOptionalAttributes(classAttributes, id, colSpan);
        Append(innerText);
        Append("</td>");
      }
    }

    public abstract class HtmlBase
    {
      private StringBuilder _sb;

      protected HtmlBase(StringBuilder sb)
      {
        _sb = sb;
      }

      public StringBuilder GetBuilder()
      {
        return _sb;
      }

      protected void Append(string toAppend)
      {
        _sb.Append(toAppend);
      }

      protected void AddOptionalAttributes(string className = "", string id = "", string colSpan = "")
      {

        if (!id.IsNullOrEmpty())
        {
          _sb.Append($" id=\"{id}\"");
        }
        if (!className.IsNullOrEmpty())
        {
          _sb.Append($" class=\"{className}\"");
        }
        if (!colSpan.IsNullOrEmpty())
        {
          _sb.Append($" colspan=\"{colSpan}\"");
        }
        _sb.Append(">");
      }
    }
  }

Usage:

StringBuilder sb = new StringBuilder();
      using (Html.Table table = new Html.Table(sb, id: "some-id"))
      {
        table.StartHead();
        using (var thead = table.AddRow())
        {
          thead.AddCell("Category Description");
          thead.AddCell("Item Description");
          thead.AddCell("Due Date");
          thead.AddCell("Amount Budgeted");
          thead.AddCell("Amount Remaining");
        }
        table.EndHead();
        table.StartBody();
        foreach (var alert in alertsForUser)
        {
          using (var tr = table.AddRow(classAttributes: "someattributes"))
          {
           tr.AddCell(alert.ExtendedInfo.CategoryDescription);
            tr.AddCell(alert.ExtendedInfo.ItemDescription);
            tr.AddCell(alert.ExtendedInfo.DueDate.ToShortDateString());
            tr.AddCell(alert.ExtendedInfo.AmountBudgeted.ToString("C"));
            tr.AddCell(alert.ExtendedInfo.ItemRemaining.ToString("C"));
          }
        }
        table.EndBody();
      }
      return sb.ToString();

Comments

5

It is a decent approach, and just 'what it takes' to output something as complicated as HTML - unless you want to do it using plain strings (which is just as messy, if not worse).

One improvement: do not use the same cell object multiple times, you run the risk of getting incorrect output. Improved code:

row.Cells.Add(new HtmlTableCell { InnerText = invalidCompany.BusinessName });
row.Cells.Add(new HtmlTableCell { InnerText = invalidCompany.SwiftBIC });
row.Cells.Add(new HtmlTableCell { InnerText = invalidCompany.IBAN });

Of course you can also create your own helpers for creating cells, for creating a row full of cells, etc. There are also good libraries for this, e.g. see https://www.nuget.org/packages/HtmlTags/.

2 Comments

@PetereB I like your approach it's much cleaner, even as you said building HTML dynamically is something that is always messy. Just one more question. How would you noted in code to go on a new row when building of one is done? This code will be in foreach loop and it will have few rows for sure. For now they just come out as one big row in table. Is it possible to say some how add new <tr> in this manner?
Double check that you are using new HtmlTableRow() every time, maybe your actual code differs from the question and somehow uses the same row for all cells? And also see my addition to the answer.
0

I think maybe you can add a function to get all the properties from your object. And then just iterate over them. Also you can create a list of properties that need to be displayed in your message.

    private static PropertyInfo[] GetProperties(object obj)
    {
        return obj.GetType().GetProperties();
    }

    // -------
    foreach (var invalidCompany in mailMessageObject.InvalidCompanies)
    {
        var properties = GetProperties(invalidCompany);     
        foreach (var p in properties)
        {   
            string name = p.Name;
            if(propertiesThatNeedToBeDisplayed.Contains(name)
            {
                cell.InnerText = p.GetValue(invalidCompany, null);
                row.Cells.Add(cell);
                table.Rows.Add(row);
            }
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.