13

I want to generate an HTML table from a couple specified parameters. Specifically, the two parameters I want to pass into my method are: IEnumerable list, and some subset of properties of T. For example, let's say I have a List of this class:

class Person
{
  string FirstName
  string MiddleName
  string LastName
}

Let's say the list has 5 people in it. I want to be able to get an HTML table of that class (or any other arbitrary class) by doing something like this:

List<Person> people;
...add people to list

string HTML = GetMyTable(people, "FirstName", "LastName");

I'm sure there's a better way to specify which properties I want the table generated from (or which properties I want excluded from the table, that would be better since I'll usually want most or all of the class's properties), but I'm not sure how (I've never used reflection, but I'm guessing that's how). Also, the method should accept a list of any type of class.

Any clever ideas on how to accomplish this?

Tim S.
  • 55,448
  • 7
  • 96
  • 122
birdus
  • 7,062
  • 17
  • 59
  • 89

6 Answers6

33

Maybe something like this?

var html = GetMyTable(people, x => x.LastName, x => x.FirstName);

public static string GetMyTable<T>(IEnumerable<T> list,params Func<T,object>[] fxns)
{

    StringBuilder sb = new StringBuilder();
    sb.Append("<TABLE>\n");
    foreach (var item in list)
    {
        sb.Append("<TR>\n");
        foreach(var fxn in fxns)
        {
            sb.Append("<TD>");
            sb.Append(fxn(item));
            sb.Append("</TD>");
        }
        sb.Append("</TR>\n");
    }
    sb.Append("</TABLE>");

    return sb.ToString();
}

--Version 2.0--

public static string GetMyTable<T>(IEnumerable<T> list, params  Expression<Func<T, object>>[] fxns)
{

    StringBuilder sb = new StringBuilder();
    sb.Append("<TABLE>\n");

    sb.Append("<TR>\n");
    foreach (var fxn in fxns)
    {
        sb.Append("<TD>");
        sb.Append(GetName(fxn));
        sb.Append("</TD>");
    }
    sb.Append("</TR> <!-- HEADER -->\n");


    foreach (var item in list)
    {
        sb.Append("<TR>\n");
        foreach (var fxn in fxns)
        {
            sb.Append("<TD>");
            sb.Append(fxn.Compile()(item));
            sb.Append("</TD>");
        }
        sb.Append("</TR>\n");
    }
    sb.Append("</TABLE>");

    return sb.ToString();
}

static string GetName<T>(Expression<Func<T, object>> expr)
{
    var member = expr.Body as MemberExpression;
    if (member != null)
        return GetName2(member);

    var unary = expr.Body as UnaryExpression;
    if (unary != null)
        return GetName2((MemberExpression)unary.Operand);

    return "?+?";
}

static string GetName2(MemberExpression member)
{
    var fieldInfo = member.Member as FieldInfo;
    if (fieldInfo != null)
    {
        var d = fieldInfo.GetCustomAttribute(typeof(DescriptionAttribute)) as DescriptionAttribute;
        if (d != null) return d.Description;
        return fieldInfo.Name;
    }

    var propertInfo = member.Member as PropertyInfo;
    if (propertInfo != null)
    {
        var d = propertInfo.GetCustomAttribute(typeof(DescriptionAttribute)) as DescriptionAttribute;
        if (d != null) return d.Description;
        return propertInfo.Name;
    }

    return "?-?";
}

PS: Calling fxn.Compile() repeatedly can be performance killer in a tight loop. It can be better to cache it in a dictionary .

L.B
  • 114,136
  • 19
  • 178
  • 224
  • What happens if you try to select an int and a string, for example? Will `P` become `object`, or will it not compile? – Tim S. Jun 20 '12 at 19:32
  • Can you explain what "fxn(item)" is doing? – birdus Jun 20 '12 at 20:37
  • It invokes the delegate expressed as lambda (`x=>x.LastName` or `x=>x.FirstName`). Since `x=>x.LastName` equals to `delegate(Person x){ return x.LastName;}` – L.B Jun 20 '12 at 20:48
  • @birdus Similar to `Func fxn = delegate(Person x){ return x.LastName;}; var lastName = fxn(person);` – L.B Jun 20 '12 at 21:00
  • Great. Thank you for those explanations. – birdus Jun 20 '12 at 21:17
  • 1
    If each property in the Person class had a [Description] attribute (using either ComponentModel or EnterpriseServices namespace), is there a way to get that text within this method, so that I can use that text to create a title/header row for my table? So the first row of the table would say "First Name | Last Name" and the next row would be "John Smith" and so on. – birdus Jun 20 '12 at 21:22
  • One other thing. GetCustomAttribute() takes two params. Suggestions on which overload I ought to use? http://msdn.microsoft.com/en-us/library/system.attribute.getcustomattribute.aspx – birdus Jun 21 '12 at 13:38
  • ...and "Compile" isn't being resolved. Where does that come from? – birdus Jun 21 '12 at 13:56
  • @birdus [LambdaExpression.Compile](http://msdn.microsoft.com/en-us/library/bb356928.aspx) – L.B Jun 21 '12 at 15:42
  • @L.B Can you explain `fxn.Compile()(item)`? When this is a list, it returns `System.Collections.Generic.List`1[System.String`. How could I make this comma delimited? – Tsukasa May 04 '16 at 17:38
  • @L.B Hello Sir, what's actually purpose of `sb.Append(fxn.Compile()(item));` and how might look example as you said stored in dictionary? Thanks – Roxy'Pro Apr 20 '20 at 07:15
13

This is what I did and it seems to work fine and not a huge performance hit.

    public static string ToHtmlTable<T>(this List<T> listOfClassObjects)
    {
        var ret = string.Empty;

        return listOfClassObjects == null || !listOfClassObjects.Any()
            ? ret
            : "<table>" +
              listOfClassObjects.First().GetType().GetProperties().Select(p => p.Name).ToList().ToColumnHeaders() +
              listOfClassObjects.Aggregate(ret, (current, t) => current + t.ToHtmlTableRow()) +
              "</table>";
    }

    public static string ToColumnHeaders<T>(this List<T> listOfProperties)
    {
        var ret = string.Empty;

        return listOfProperties == null || !listOfProperties.Any()
            ? ret
            : "<tr>" +
              listOfProperties.Aggregate(ret,
                  (current, propValue) =>
                      current +
                      ("<th style='font-size: 11pt; font-weight: bold; border: 1pt solid black'>" +
                       (Convert.ToString(propValue).Length <= 100
                           ? Convert.ToString(propValue)
                           : Convert.ToString(propValue).Substring(0, 100)) + "..." + "</th>")) +
              "</tr>";
    }

    public static string ToHtmlTableRow<T>(this T classObject)
    {
        var ret = string.Empty;

        return classObject == null
            ? ret
            : "<tr>" +
              classObject.GetType()
                  .GetProperties()
                  .Aggregate(ret,
                      (current, prop) =>
                          current + ("<td style='font-size: 11pt; font-weight: normal; border: 1pt solid black'>" +
                                     (Convert.ToString(prop.GetValue(classObject, null)).Length <= 100
                                         ? Convert.ToString(prop.GetValue(classObject, null))
                                         : Convert.ToString(prop.GetValue(classObject, null)).Substring(0, 100) +
                                           "...") +
                                     "</td>")) + "</tr>";
    }

To use it just pass the ToHtmlTable() a List Example:

List documents = GetMyListOfDocuments(); var table = documents.ToHtmlTable();

VirDeus
  • 131
  • 1
  • 2
4

Here are two approaches, one using reflection:

public static string GetMyTable(IEnumerable list, params string[] columns)
{
    var sb = new StringBuilder();
    foreach (var item in list)
    {
        //todo this should actually make an HTML table, not just get the properties requested
        foreach (var column in columns)
            sb.Append(item.GetType().GetProperty(column).GetValue(item, null));
    }
    return sb.ToString();
}
//used like
string HTML = GetMyTable(people, "FirstName", "LastName");

Or using lambdas:

public static string GetMyTable<T>(IEnumerable<T> list, params Func<T, object>[] columns)
{
    var sb = new StringBuilder();
    foreach (var item in list)
    {
        //todo this should actually make an HTML table, not just get the properties requested
        foreach (var column in columns)
            sb.Append(column(item));
    }
    return sb.ToString();
}
//used like
string HTML = GetMyTable(people, x => x.FirstName, x => x.LastName);

With the lambdas, what's happening is you're passing methods to the GetMyTable method to get each property. This has benefits over reflection like strong typing, and probably performance.

Tim S.
  • 55,448
  • 7
  • 96
  • 122
  • Thanks for the help, Tim. I appreciate seeing multiple ways to doing this. – birdus Jun 21 '12 at 13:31
  • the lambda approach seems to be significantly better in terms of performance. my objective is to export all properties, rather than just individual ones, how could i achieve that using the lambda method, without having to explicitly list all of the properties – thehill Oct 10 '21 at 07:27
4

Good luck with

Extention Method

  public static class EnumerableExtension
{
    public static string ToHtmlTable<T>(this IEnumerable<T> list, List<string> headerList, List<CustomTableStyle> customTableStyles, params Func<T, object>[] columns)
    {
        if (customTableStyles == null)
            customTableStyles = new List<CustomTableStyle>();

        var tableCss = string.Join(" ", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Table).Where(w => w.ClassNameList != null).SelectMany(s => s.ClassNameList)) ?? "";
        var trCss = string.Join(" ", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Tr).Where(w => w.ClassNameList != null).SelectMany(s => s.ClassNameList)) ?? "";
        var thCss = string.Join(" ", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Th).Where(w => w.ClassNameList != null).SelectMany(s => s.ClassNameList)) ?? "";
        var tdCss = string.Join(" ", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Td).Where(w => w.ClassNameList != null).SelectMany(s => s.ClassNameList)) ?? "";

        var tableInlineCss = string.Join(";", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Table).Where(w => w.InlineStyleValueList != null).SelectMany(s => s.InlineStyleValueList?.Select(x => String.Format("{0}:{1}", x.Key, x.Value)))) ?? "";
        var trInlineCss = string.Join(";", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Tr).Where(w => w.InlineStyleValueList != null).SelectMany(s => s.InlineStyleValueList?.Select(x => String.Format("{0}:{1}", x.Key, x.Value)))) ?? "";
        var thInlineCss = string.Join(";", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Th).Where(w => w.InlineStyleValueList != null).SelectMany(s => s.InlineStyleValueList?.Select(x => String.Format("{0}:{1}", x.Key, x.Value)))) ?? "";
        var tdInlineCss = string.Join(";", customTableStyles?.Where(w => w.CustomTableStylePosition == CustomTableStylePosition.Td).Where(w => w.InlineStyleValueList != null).SelectMany(s => s.InlineStyleValueList?.Select(x => String.Format("{0}:{1}", x.Key, x.Value)))) ?? "";

        var sb = new StringBuilder();

        sb.Append($"<table{(string.IsNullOrEmpty(tableCss) ? "" : $" class=\"{tableCss}\"")}{(string.IsNullOrEmpty(tableInlineCss) ? "" : $" style=\"{tableInlineCss}\"")}>");
        if (headerList != null)
        {
            sb.Append($"<tr{(string.IsNullOrEmpty(trCss) ? "" : $" class=\"{trCss}\"")}{(string.IsNullOrEmpty(trInlineCss) ? "" : $" style=\"{trInlineCss}\"")}>");
            foreach (var header in headerList)
            {
                sb.Append($"<th{(string.IsNullOrEmpty(thCss) ? "" : $" class=\"{thCss}\"")}{(string.IsNullOrEmpty(thInlineCss) ? "" : $" style=\"{thInlineCss}\"")}>{header}</th>");
            }
            sb.Append("</tr>");
        }
        foreach (var item in list)
        {
            sb.Append($"<tr{(string.IsNullOrEmpty(trCss) ? "" : $" class=\"{trCss}\"")}{(string.IsNullOrEmpty(trInlineCss) ? "" : $" style=\"{trInlineCss}\"")}>");
            foreach (var column in columns)
                sb.Append($"<td{(string.IsNullOrEmpty(tdCss) ? "" : $" class=\"{tdCss}\"")}{(string.IsNullOrEmpty(tdInlineCss) ? "" : $" style=\"{tdInlineCss}\"")}>{column(item)}</td>");
            sb.Append("</tr>");
        }

        sb.Append("</table>");

        return sb.ToString();
    }

    public class CustomTableStyle
    {
        public CustomTableStylePosition CustomTableStylePosition { get; set; }

        public List<string> ClassNameList { get; set; }
        public Dictionary<string, string> InlineStyleValueList { get; set; }
    }

    public enum CustomTableStylePosition
    {
        Table,
        Tr,
        Th,
        Td
    }
}

Using

 private static void Main(string[] args)
    {
        var dataList = new List<TestDataClass>
        {
            new TestDataClass {Name = "A", Lastname = "B", Other = "ABO"},
            new TestDataClass {Name = "C", Lastname = "D", Other = "CDO"},
            new TestDataClass {Name = "E", Lastname = "F", Other = "EFO"},
            new TestDataClass {Name = "G", Lastname = "H", Other = "GHO"}
        };

        var headerList = new List<string> { "Name", "Surname", "Merge" };

        var customTableStyle = new List<EnumerableExtension.CustomTableStyle>
        {
            new EnumerableExtension.CustomTableStyle{CustomTableStylePosition = EnumerableExtension.CustomTableStylePosition.Table, InlineStyleValueList = new Dictionary<string, string>{{"font-family", "Comic Sans MS" },{"font-size","15px"}}},
            new EnumerableExtension.CustomTableStyle{CustomTableStylePosition = EnumerableExtension.CustomTableStylePosition.Table, InlineStyleValueList = new Dictionary<string, string>{{"background-color", "yellow" }}},
            new EnumerableExtension.CustomTableStyle{CustomTableStylePosition = EnumerableExtension.CustomTableStylePosition.Tr, InlineStyleValueList =new Dictionary<string, string>{{"color","Blue"},{"font-size","10px"}}},
            new EnumerableExtension.CustomTableStyle{CustomTableStylePosition = EnumerableExtension.CustomTableStylePosition.Th,ClassNameList = new List<string>{"normal","underline"}},
            new EnumerableExtension.CustomTableStyle{CustomTableStylePosition = EnumerableExtension.CustomTableStylePosition.Th,InlineStyleValueList =new Dictionary<string, string>{{ "background-color", "gray"}}},
            new EnumerableExtension.CustomTableStyle{CustomTableStylePosition = EnumerableExtension.CustomTableStylePosition.Td, InlineStyleValueList  =new Dictionary<string, string>{{"color","Red"},{"font-size","15px"}}},
        };

        var htmlResult = dataList.ToHtmlTable(headerList, customTableStyle, x => x.Name, x => x.Lastname, x => $"{x.Name} {x.Lastname}");
    }

    private class TestDataClass
    {
        public string Name { get; set; }
        public string Lastname { get; set; }
        public string Other { get; set; }
    }

Result

<table class="normal underline" style="font-family:Comic Sans MS;font-size:15px;background-color:yellow">
<tr style="color:Blue;font-size:10px">
    <th style="background-color:gray">Name</th>
    <th style="background-color:gray">Surname</th>
    <th style="background-color:gray">Merge</th>
</tr>
<tr style="color:Blue;font-size:10px">
    <td style="color:Red;font-size:15px">A</td>
    <td style="color:Red;font-size:15px">B</td>
    <td style="color:Red;font-size:15px">A B</td>
</tr>
<tr style="color:Blue;font-size:10px">
    <td style="color:Red;font-size:15px">C</td>
    <td style="color:Red;font-size:15px">D</td>
    <td style="color:Red;font-size:15px">C D</td>
</tr>
<tr style="color:Blue;font-size:10px">
    <td style="color:Red;font-size:15px">E</td>
    <td style="color:Red;font-size:15px">F</td>
    <td style="color:Red;font-size:15px">E F</td>
</tr>
<tr style="color:Blue;font-size:10px">
    <td style="color:Red;font-size:15px">G</td>
    <td style="color:Red;font-size:15px">H</td>
    <td style="color:Red;font-size:15px">G H</td
</tr>

0

Extending The answer of @Tim

    public string GetHtmlTable<T, Tproperty>(IEnumerable<T> list, params Expression<Func<T, Tproperty>>[] columns)
    {
        var sb = new StringBuilder();


        sb.AppendLine("<table>");
        sb.AppendLine("<tr>");
        foreach (var column in columns)
        {
            sb.Append("<th>");
            sb.Append(GetPropertyName(Activator.CreateInstance<T>(), column));
            sb.Append("</th>");
        }
        sb.AppendLine("</tr>");
        sb.AppendLine("<tbody>");

        foreach (var item in list)
        {
            sb.AppendLine("<tr>");

            foreach (var column in columns)
            {
                var func = column.Compile();
                sb.Append("<td>");
                sb.Append(func(item));
                sb.Append("</td>");

            }
            sb.AppendLine("</tr>");
        }
        sb.AppendLine("</tbody>");
        sb.AppendLine("</table>");

        return sb.ToString();
    }






    public string GetPropertyName<TSource, TProperty>(TSource source, Expression<Func<TSource, TProperty>> propertyLambda)
    {
        Type type = typeof(TSource);


        var expressionBody = propertyLambda.Body;
        if (expressionBody is UnaryExpression expression && expression.NodeType == ExpressionType.Convert)
        {
            expressionBody = expression.Operand;
        }


        MemberExpression member = (MemberExpression)expressionBody;
        if (member == null)
            return "";

        PropertyInfo propInfo = member.Member as PropertyInfo;
        if (propInfo == null)
            return "";

        if (type != propInfo.ReflectedType &&
            !type.IsSubclassOf(propInfo.ReflectedType)) return "";

        return propInfo.Name;
    }
}
Naveed Yousaf
  • 436
  • 4
  • 14
0

This is the improved solution from L.B answer, it supports table header , using short clean code and can detect by Intellisense if you use wrong properties name

To use the function

List<Person> peoples = new List<Person>();
// ...add multiple Person to list<Person>
string HTML_Table = GetMyTable(peoples, x => x.FirstName, x => x.LastName);

The generic function looks like this...

using System.Text;
using System.Reflection;

    public static string GetMyTable<T>(IEnumerable<T> list,params Func<T,object>[] fxns)
    {
        if (list.Count() == 0)
        { return "";}

        // Dynamic build  HTML Table Header column name base on T\model object
        Type type = list.FirstOrDefault().GetType();
        PropertyInfo[] props = type.GetProperties();
        string THstr = "<tr>";
        foreach (var prop in props)
        {
            THstr+= "<TH>" + prop.Name + "</TH>";
        }
        THstr += "</tr>";

        // Build  remain data rows in HTML Table
        StringBuilder sb = new StringBuilder();
        // Inject bootstrap class base on your need
        sb.Append("<TABLE class='table table-sm table-dark'>\n");
        sb.Append(THstr);
        foreach (var item in list)
        {
            sb.Append("<TR>\n");
            foreach(var fxn in fxns)
            {
                sb.Append("<TD>");
                sb.Append(fxn(item));
                sb.Append("</TD>");
            }
            sb.Append("</TR>\n");
        }
        sb.Append("</TABLE>");

        return sb.ToString();
}
HO LI Pin
  • 1,441
  • 13
  • 13