9

I am trying to create a user control that accepts any generic list so I can iterate through it and create a CSV export. Is it possible to expose a public property that can accept any type? (i.e. List<Product>, List<Customer>, etc.) If yes, how?

public IEnumerable<T> AnyList { get; set; }

Here's what I have as far as the utility methods I have:

public static byte[] ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    //Type t = typeof(T); Deleted this line.

    //Here's the line of code updated.
    PropertyInfo[] propertyNames = objectlist.First().GetType().GetProperties();

    string header = String.Join(separator, propertyNames.Select(f => f.Name).ToArray());

    StringBuilder csvdata = new StringBuilder();
    csvdata.AppendLine(header);

    foreach (var o in objectlist)
        csvdata.AppendLine(ToCsvFields(separator, propertyNames, o));

    return Encoding.ASCII.GetBytes(csvdata.ToString());
}

public static string ToCsvFields(string separator, PropertyInfo[] fields, object o)
{
    StringBuilder linie = new StringBuilder();

    foreach (var f in fields)
    {
        if (linie.Length > 0)
            linie.Append(separator);

        var x = f.GetValue(o, null);

        if (x != null)
            linie.Append(x.ToString());
    }

    return linie.ToString();
}
Chris Pfohl
  • 18,220
  • 9
  • 68
  • 111
Rod
  • 14,529
  • 31
  • 118
  • 230
  • 1
    I assume you're not actually asking "is it possible". You may want to (read: you ought to) edit your question and title to explicitly ask *how* to do it – Chris Pfohl Jun 06 '12 at 14:20
  • How are you going to convert each `T` to a string? If you're simply going to call `ToString()` on each item, perhaps `public IEnumerable AnyList { get; set; }` will suffice. – Michael Liu Jun 06 '12 at 14:26

6 Answers6

17

As you want to "Create a CSV export" from a list of objects, you should be using reflection to work out the columns.

Latest update 12 Feb 2016:

It makes more sense to have the delimiter default to a comma, and useful to make the output of an initial header row optional. It also now supports both fields and simple properties by use of Concat:

public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    if (header)
    {
        yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    }
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

so you then use it like this for comma delimited:

foreach (var line in ToCsv(objects))
{
    Console.WriteLine(line);
}

or like this for another delimiter (e.g. TAB):

foreach (var line in ToCsv(objects, "\t"))
{
    Console.WriteLine(line);
}

Practical examples

write list to a comma-delimited CSV file

using (TextWriter tw = File.CreateText("C:\testoutput.csv"))
{
    foreach (var line in ToCsv(objects))
    {
        tw.WriteLine(line);
    }
}

or write it tab-delimited

using (TextWriter tw = File.CreateText("C:\testoutput.txt"))
{
    foreach (var line in ToCsv(objects, "\t"))
    {
        tw.WriteLine(line);
    }
}

If you have complex fields/properties you will need to filter them out of the select clauses.


Previous updates

Final thoughts first (so it is not missed):

If you prefer a generic solution (this ensures the objects are of the same type):

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    yield return String.Join(separator, fields.Select(f => f.Name).Union(properties.Select(p=>p.Name)).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Union(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

This one includes both public fields and public properties.

In general with reflection you do not need to know the type of objects in the list (you just must assume they are all the same type).

You could just use:

public IEnumerable<object> AnyList { get; set; }

The basic process for what you want to do goes something like:

  • Obtain the type from the first object in the list (e.g. GetType()).
  • Iterate the properties of that type.
  • Write out the CSV header, e.g. based on the names of the property (or an attribute).
  • For each item in the list...
    • Iterate the properties of that type
    • Get the value for each property (as an object)
    • Write out the ToString() version of the object with delimiters

You can use the same algorithm to generate a 2D array (i.e. if you want the control to display something like CSV in tabular/grid form).

The only issue you than have may be converting from IEnumerables/lists of specific types to an IEnumerable. In these instances just use .Cast<object> on your specific typed enumerable.

Update:

As you are using code from http://www.joe-stevens.com/2009/08/03/generate-a-csv-from-a-generic-list-of-objects-using-reflection-and-extension-methods/

You need to make the following change to his code:

// Make it a simple extension method for a list of object
public static string GetCSV(this List<object> list)
{
    StringBuilder sb = new StringBuilder();

    //Get the properties from the first object in the list for the headers
    PropertyInfo[] propInfos = list.First().GetType().GetProperties();

If you want to support an empty list, add second parameter (e.g. Type type) which is the type of object you expected and use that instead of list.First().GetType().

note: I don't see anywhere else in his code where T is referenced, but if I missed it the compiler will find it for you :)

Update (complete & simplified CSV generator):

public static IEnumerable<string> ToCsv(string separator, IEnumerable<object> objectlist)
{
    if (objectlist.Any())
    {
        Type type = objectlist.First().GetType();
        FieldInfo[] fields = type.GetFields();
        yield return String.Join(separator, fields.Select(f => f.Name).ToArray());
        foreach (var o in objectlist)
        {
            yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString()).ToArray());
        }
    }
}

This has the benefit of a low memory overhead as it yields results as they occur, rather than create a massive string. You can use it like:

foreach (var line in ToCsv(",", objects))
{
    Console.WriteLine(line);
}

I prefer the generic solution in practice so have placed that first.

davidmdem
  • 3,763
  • 2
  • 30
  • 33
iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • I'm using the following code snippet to convert my IEnumerable to CSV: http://www.joe-stevens.com/2009/08/03/generate-a-csv-from-a-generic-list-of-objects-using-reflection-and-extension-methods/ – Rod Jun 06 '12 at 15:53
  • Continued from last comment: If I explicitly pass List to the method it works fine, but if I pass IEnumerable I get blank results. If I step through the code I can see that my customers are there. So I'm missing something between your code and the code in the article. Please advise. – Rod Jun 06 '12 at 15:53
  • 2
    That's what happens when you take someone's else code off the shelf :) I will update the answer with a modified version of his code, but basically there is only a minor change needed. – iCollect.it Ltd Jun 06 '12 at 16:10
  • wait, let me post the code I have it may not be exactly what's in the article. I actually added this post a while ago just under the wrong section. sorry. – Rod Jun 06 '12 at 18:01
  • I took the info that you provided in the update and implemented into what I posted in OP and it works now. So like you said, had to change in one place. See comment where I changed. Thank you. If anyone has additional commnents or concerns please advise. – Rod Jun 06 '12 at 18:20
  • Cont'd from my last comment - could you help me understand the comments made about the answer in the link above regarding possible out-of-memory issues? Also, please see my comments under my OP at this link: http://stackoverflow.com/questions/10762622/get-property-name-from-generic-list – Rod Jun 07 '12 at 04:14
  • 1
    The only memory issue I can see in that example is a possibly massive string being returned (if it were for millions of records). The workaround for that would be to return an `IEnumerable` and `yield return` each line of the CSV, one at a time instead. Then no memory problem (at least a that point) as you will process the CSV output line by line. Let me know if you want me to update the answer with a sample rewrite. – iCollect.it Ltd Jun 07 '12 at 09:13
  • Thank you very much for your insights and patience. I appreciate it. And thank you everyone for your feedback and perspectives. – Rod Jun 07 '12 at 13:11
  • FieldInfo[] fields = type.GetFields(); The preceding line gives me an empty array. If you'll notice that the CSV code I found originally also used FieldInfo[] and I was debugging and found that PropertyInfo worked for me (not sure why one works and not the other) but I tried to do the same with your snippet and got stuck with new f.GetValue(o) wants object[] index parm. – Rod Jun 07 '12 at 13:55
  • 1
    That was down to the example you used assuming only fields... If you have fields, use GetFields(), if you have get/set properties, use PropertyInfo (but you must provide a null index to GetValue and it only works for basic properties)... in theory you should use both and return them all, but filter for only public properties or you may get field/prop clashes. I will update the answer... – iCollect.it Ltd Jun 07 '12 at 13:59
  • 1
    Updated with (hopefully) final version :) If you have complex types as fields/properties you will need to add a filter to exclude those. Good luck! – iCollect.it Ltd Jun 07 '12 at 14:20
  • Thanks again so much, this has been truly educational. I am now starting to understand the yield keyword from this exercise. – Rod Jun 07 '12 at 14:26
  • In last version of answer ToCsv has 3 parameters but sample call form answer calls it with two parameters only – Andrus Aug 20 '14 at 19:55
  • @Andrus: Thanks, adjusted answer. That was for an earlier example proposed to take a type as a parameter (obviously not relevant for the final generic version). – iCollect.it Ltd Aug 21 '14 at 08:21
2

You would have to do something like this:

public class MyUserControl<T> : UserControl
{
    public IEnumerable<T> AnyList { get; set; }
}

From a C# view, this is perfectly fine. However, if you want to be able to declare your user control in ASPX Markup, then a user control cannot contain class-level type parameters. This is not possible:

<controls:MyUserControl<Customer> runat="server" ID="Foo" />

Now, if your user control will only be created in the code-behind, and not in markup, then this is fine:

this.SomePlaceHolder.Controls.Add(new MyUserControl<Customer>());

If you need / want the ability to declare your user control in ASPX markup, the best you could do is create an IEnumerable<object> property.

vcsjones
  • 138,677
  • 31
  • 291
  • 286
  • I'm using the following code snippet to convert my IEnumerable to CSV: http://www.joe-stevens.com/2009/08/03/generate-a-csv-from-a-generic-list-of-objects-using-reflection-and-extension-methods/ – Rod Jun 06 '12 at 15:47
  • @rod then you probably don't even care about the type at compile time. Just use `IEnumerable`. The code you linked to is using reflection. – vcsjones Jun 06 '12 at 15:48
  • Continued from last comment: If I explicitly pass List to the method it works fine, but if I pass IEnumerable I get blank results. If I step through the code I can see that my customers are there. So I'm missing something between your code and the code in the article. Please advise. – Rod Jun 06 '12 at 15:49
  • I apologize I put comments under wrong section. I was trying one of the code snippets in another answer so it didn't apply here. But if it makes sense please do advise. – Rod Jun 06 '12 at 15:55
1

I found a nice solution from here

public static class LinqToCSV
{
    public static string ToCsv<T>(this IEnumerable<T> items)
        where T : class
    {
        var csvBuilder = new StringBuilder();
        var properties = typeof(T).GetProperties();
        foreach (T item in items)
        {
            string line = string.Join(",",properties.Select(p => p.GetValue(item, null).ToCsvValue()).ToArray());
            csvBuilder.AppendLine(line);
        }
        return csvBuilder.ToString();
    }

    private static string ToCsvValue<T>(this T item)
    {
        if(item == null) return "\"\"";

        if (item is string)
        {
            return string.Format("\"{0}\"", item.ToString().Replace("\"", "\\\""));
        }
        double dummy;
        if (double.TryParse(item.ToString(), out dummy))
        {
            return string.Format("{0}", item);
        }
        return string.Format("\"{0}\"", item);
    }
}

Apparently it handles commas and double quotes. I'm gonna use it.

Aximili
  • 28,626
  • 56
  • 157
  • 216
0

To have generic properties, the class must be generic too:

public class MyClass<T>
{
   public IEnumerable<T> AnyList { get; set; }
}
Vishal Mistry
  • 376
  • 1
  • 5
0

Since IEnumerable is a base class of IEnumerable<T>, any properly written IEnumerable<T> class could be passed to a property of type IEnumerable. Since it sounds like you are only interested in the "ToString() part", and not the "T part", IEnumerable should work just fine.

jyoung
  • 5,071
  • 4
  • 30
  • 47
0

This is working for me.

using System.Reflection;
using System.Text;
//***    


public void ToCSV<T>(IEnumerable<T> items, string filePath)
{
    var dataTable = new DataTable(typeof(T).Name);
    PropertyInfo[] props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
    foreach (var prop in props)
        dataTable.Columns.Add(prop.Name, prop.PropertyType);

    foreach (var item in items)
    {
        var values = new object[props.Length];
        for (var i = 0; i < props.Length; i++)
        {
            values[i] = props[i].GetValue(item, null);
        }
        dataTable.Rows.Add(values);
    }

    StringBuilder fileContent = new StringBuilder();
    foreach (var col in dataTable.Columns)
        fileContent.Append(col.ToString() + ",");

    fileContent.Replace(",", System.Environment.NewLine, fileContent.Length - 1, 1);

    foreach (DataRow dr in dataTable.Rows)
    {
        foreach (var column in dr.ItemArray)
            fileContent.Append("\"" + column.ToString() + "\",");

        fileContent.Replace(",", System.Environment.NewLine, fileContent.Length - 1, 1);
    }

    try
    {
        System.IO.File.WriteAllText(filePath, fileContent.ToString());
    }
    catch (Exception)
    {

        throw;
    }
}
Carlos Toledo
  • 2,519
  • 23
  • 23