2015-10-30 5 views
10

EPPlus verfügt über eine bequeme Methode LoadFromCollection<T>, um Daten meines eigenen Typs in ein Arbeitsblatt zu bekommen.So analysieren Sie Excel-Zeilen zurück zu Typen mit EPPlus

Zum Beispiel, wenn ich eine Klasse haben:

public class Customer 
{ 
    public int Id { get; set; } 
    public string Firstname { get; set; } 
    public string Surname { get; set; } 
    public DateTime Birthdate { get; set; } 
} 

dann den folgenden Code:

var package = new ExcelPackage(); 
var sheet = package.Workbook.Worksheets.Add("Customers"); 
var customers = new List<Customer>{ 
    new Customer{ 
     Id = 1, 
     Firstname = "John", 
     Surname = "Doe", 
     Birthdate = new DateTime(2000, 1, 1) 
    }, 
    new Customer{ 
     Id = 2, 
     Firstname = "Mary", 
     Surname = "Moe", 
     Birthdate = new DateTime(2001, 2, 2) 
    } 
}; 
sheet.Cells[1, 1].LoadFromCollection(customers); 
package.Save(); 

... werden zwei Zeilen in einem Arbeitsblatt "Kunden" genannt hinzuzufügen.

Meine Frage ist, ob es ein praktisches Gegenstück zum Extrahieren der Zeilen von Excel (zum Beispiel nachdem einige Änderungen vorgenommen wurden) zurück in meine Typen gibt.

Etwas wie:

var package = new ExcelPackage(inputStream); 
var customers = sheet.Dimension.SaveToCollection<Customer>() ?? 

Ich habe

  • durch die EPPlus Code-Basis suchen worden
  • für alle saving Fragen gesucht
  • für alle parsing Fragen gesucht
  • gesehen this Frage beim Einlesen g einzelne Zellen

... aber nichts gefunden, wie man die Zeilen einfach zu meinem Typ analysiert.

Antwort

18

Inspiriert von den oben genannten nahm ich es eine etwas andere Route.

  1. Ich habe ein Attribut erstellt und jede Eigenschaft einer Spalte zugeordnet.
  2. Ich verwende den Typ DTO zu definieren, was ich jede Spalte erwarten nicht
  3. Verwenden EPPlus requried werden, um sein
  4. zulassen Spalten die Typen

Damit es erlaubt mir zu konvertieren traditionelle zu verwenden Modellvalidierung und umfassen Änderungen an Spaltenüberschriften

- Verbrauch:

using(FileStream fileStream = new FileStream(_fileName, FileMode.Open)){ 
     ExcelPackage excel = new ExcelPackage(fileStream); 
     var workSheet = excel.Workbook.Worksheets[RESOURCES_WORKSHEET]; 

     IEnumerable<ExcelResourceDto> newcollection = workSheet.ConvertSheetToObjects<ExcelResourceDto>(); 
     newcollection.ToList().ForEach(x => Console.WriteLine(x.Title)); 
} 

Dto die Karten

public class ExcelResourceDto 
{ 
    [Column(1)] 
    [Required] 
    public string Title { get; set; } 

    [Column(2)] 
    [Required] 
    public string SearchTags { get; set; } 
} 

Dies ist das Attribut Definition

[AttributeUsage(AttributeTargets.All)] 
public class Column : System.Attribute 
{ 
    public int ColumnIndex { get; set; } 


    public Column(int column) 
    { 
     ColumnIndex = column; 
    } 
} 

Erweiterung Klasse auszuzeichnen Mapping Zeilen DTO

public static class EPPLusExtensions 
{ 
    public static IEnumerable<T> ConvertSheetToObjects<T>(this ExcelWorksheet worksheet) where T : new() 
    { 

     Func<CustomAttributeData, bool> columnOnly = y => y.AttributeType == typeof(Column); 

     var columns = typeof(T) 
       .GetProperties() 
       .Where(x => x.CustomAttributes.Any(columnOnly)) 
     .Select(p => new 
     { 
      Property = p, 
      Column = p.GetCustomAttributes<Column>().First().ColumnIndex //safe because if where above 
     }).ToList(); 


     var rows= worksheet.Cells 
      .Select(cell => cell.Start.Row) 
      .Distinct() 
      .OrderBy(x=>x); 


     //Create the collection container 
     var collection = rows.Skip(1) 
      .Select(row => 
      { 
       var tnew = new T(); 
       columns.ForEach(col => 
       { 
        //This is the real wrinkle to using reflection - Excel stores all numbers as double including int 
        var val = worksheet.Cells[row, col.Column]; 
        //If it is numeric it is a double since that is how excel stores all numbers 
        if (val.Value == null) 
        { 
         col.Property.SetValue(tnew, null); 
         return; 
        } 
        if (col.Property.PropertyType == typeof(Int32)) 
        { 
         col.Property.SetValue(tnew, val.GetValue<int>()); 
         return; 
        } 
        if (col.Property.PropertyType == typeof(double)) 
        { 
         col.Property.SetValue(tnew, val.GetValue<double>()); 
         return; 
        } 
        if (col.Property.PropertyType == typeof(DateTime)) 
        { 
         col.Property.SetValue(tnew, val.GetValue<DateTime>()); 
         return; 
        } 
        //Its a string 
        col.Property.SetValue(tnew, val.GetValue<string>()); 
       }); 

       return tnew; 
      }); 


     //Send it back 
     return collection; 
    } 
} 
+2

Funktioniert gut. Hinweis für andere: 'const int RESOURCES_WORKSHEET = 1' (Worksheet-Indizes sind 1-basiert) – fiat

+2

Es ist einfach, dies so zu ändern, dass das Spaltenattribut den Spaltennamen anstelle des Spaltenindex angibt. – subsci

+0

Ich suchte nach einer effizienten Möglichkeit, die PropertyInfo und die Spalte in eine Liste zu mappen, nette Verwendung von 'Func' und nette Verwendung von Linq, ich habe den Zeitaufwand für die gleiche Operation in 7,5% reduziert, was nicht scheint viel, aber es geht von 680k ms zu 50k ms Diese Lösung wäre schneller, wenn Sie Parallel.foreach anwenden. Ich habe viel aus dieser Antwort gelernt. Vielen Dank! – Nekeniehl

4

Es gibt leider keine solche Methode in EPPlus. Es ist eine harte Nuss zu knacken, da Sie Reflexionen verwenden müssen, wenn Sie wirklich wollen, dass es generisch ist. Und weil Excel alle Zahlen und Daten doppelt speichert, müssen Sie mit vielen Unboxing- und Typ-Checks fertig werden.

Das ist etwas, woran ich gearbeitet habe. Es ist eine Erweiterungsmethode, die es über Generics tun wird. Es funktioniert aber nur unter begrenzten Tests, also stellen Sie sicher, dass Sie es selbst überprüfen. Ich kann nicht garantieren, dass es (noch) am besten optimiert ist, aber es ist ziemlich anständig in seinem Punkt. Sie würde es so verwenden:

IEnumerable<TestObject> newcollection = worksheet.ConvertSheetToObjects<TestObject>(); 

Die Erweiterung:

public static IEnumerable<T> ConvertSheetToObjects<T>(this ExcelWorksheet worksheet) where T:new() 
{ 
    //DateTime Conversion 
    var convertDateTime = new Func<double, DateTime>(excelDate => 
    { 
     if (excelDate < 1) 
      throw new ArgumentException("Excel dates cannot be smaller than 0."); 

     var dateOfReference = new DateTime(1900, 1, 1); 

     if (excelDate > 60d) 
      excelDate = excelDate - 2; 
     else 
      excelDate = excelDate - 1; 
     return dateOfReference.AddDays(excelDate); 
    }); 

    //Get the properties of T 
    var tprops = (new T()) 
     .GetType() 
     .GetProperties() 
     .ToList(); 

    //Cells only contains references to cells with actual data 
    var groups = worksheet.Cells 
     .GroupBy(cell => cell.Start.Row) 
     .ToList(); 

    //Assume the second row represents column data types (big assumption!) 
    var types = groups 
     .Skip(1) 
     .First() 
     .Select(rcell => rcell.Value.GetType()) 
     .ToList(); 

    //Assume first row has the column names 
    var colnames = groups 
     .First() 
     .Select((hcell, idx) => new { Name = hcell.Value.ToString(), index = idx }) 
     .Where(o => tprops.Select(p => p.Name).Contains(o.Name)) 
     .ToList(); 

    //Everything after the header is data 
    var rowvalues = groups 
     .Skip(1) //Exclude header 
     .Select(cg => cg.Select(c => c.Value).ToList()); 


    //Create the collection container 
    var collection = rowvalues 
     .Select(row => 
     { 
      var tnew = new T(); 
      colnames.ForEach(colname => 
      { 
       //This is the real wrinkle to using reflection - Excel stores all numbers as double including int 
       var val = row[colname.index]; 
       var type = types[colname.index]; 
       var prop = tprops.First(p => p.Name == colname.Name); 

       //If it is numeric it is a double since that is how excel stores all numbers 
       if (type == typeof (double)) 
       { 
        //Unbox it 
        var unboxedVal = (double) val; 

        //FAR FROM A COMPLETE LIST!!! 
        if (prop.PropertyType == typeof (Int32)) 
         prop.SetValue(tnew, (int) unboxedVal); 
        else if (prop.PropertyType == typeof (double)) 
         prop.SetValue(tnew, unboxedVal); 
        else if (prop.PropertyType == typeof (DateTime)) 
         prop.SetValue(tnew, convertDateTime(unboxedVal)); 
        else 
         throw new NotImplementedException(String.Format("Type '{0}' not implemented yet!", prop.PropertyType.Name)); 
       } 
       else 
       { 
        //Its a string 
        prop.SetValue(tnew, val); 
       } 
      }); 

      return tnew; 
     }); 


    //Send it back 
    return collection; 
} 

A FULL Beispiel:

[TestMethod] 
public void Read_To_Collection_Test() 
{ 
    //A collection to Test 
    var objectcollection = new List<TestObject>(); 

    for (var i = 0; i < 10; i++) 
     objectcollection.Add(new TestObject {Col1 = i, Col2 = i*10, Col3 = Path.GetRandomFileName(), Col4 = DateTime.Now.AddDays(i)}); 

    //Create a test file to convert back 
    byte[] bytes; 
    using (var pck = new ExcelPackage()) 
    { 
     //Load the random data 
     var workbook = pck.Workbook; 
     var worksheet = workbook.Worksheets.Add("data"); 
     worksheet.Cells.LoadFromCollection(objectcollection, true); 
     bytes = pck.GetAsByteArray(); 
    } 


    //********************************* 
    //Convert from excel to a collection 
    using (var pck = new ExcelPackage(new MemoryStream(bytes))) 
    { 
     var workbook = pck.Workbook; 
     var worksheet = workbook.Worksheets["data"]; 

     var newcollection = worksheet.ConvertSheetToObjects<TestObject>(); 
     newcollection.ToList().ForEach(to => Console.WriteLine("{{ Col1:{0}, Col2: {1}, Col3: \"{2}\", Col4: {3} }}", to.Col1, to.Col2, to.Col3, to.Col4.ToShortDateString())); 
    } 
} 

//test object class 
public class TestObject 
{ 
    public int Col1 { get; set; } 
    public int Col2 { get; set; } 
    public string Col3 { get; set; } 
    public DateTime Col4 { get; set; } 
} 

Konsolenausgabe:

{ Col1:0, Col2: 0, Col3: "wrulvxbx.wdv", Col4: 10/30/2015 } 
{ Col1:1, Col2: 10, Col3: "wflh34yu.0pu", Col4: 10/31/2015 } 
{ Col1:2, Col2: 20, Col3: "ps0f1jg0.121", Col4: 11/1/2015 } 
{ Col1:3, Col2: 30, Col3: "skoc2gx1.2xs", Col4: 11/2/2015 } 
{ Col1:4, Col2: 40, Col3: "urs3jnbb.ob1", Col4: 11/3/2015 } 
{ Col1:5, Col2: 50, Col3: "m4l2fese.4yz", Col4: 11/4/2015 } 
{ Col1:6, Col2: 60, Col3: "v3dselpn.rqq", Col4: 11/5/2015 } 
{ Col1:7, Col2: 70, Col3: "v2ggbaar.r31", Col4: 11/6/2015 } 
{ Col1:8, Col2: 80, Col3: "da4vd35p.msl", Col4: 11/7/2015 } 
{ Col1:9, Col2: 90, Col3: "v5dtpuad.2ao", Col4: 11/8/2015 } 
+0

Ehrfürchtig @Ernie, thanks for sharing zu handhaben. In der Tat, einige Annahmen, aber gibt einen großen Einblick, wie dies zu lösen ist. –

Verwandte Themen