2015-10-30 20 views
10

EPPlus dispone di un metodo conveniente LoadFromCollection<T> per ottenere dati del mio tipo in un foglio di lavoro.Come analizzare nuovamente le righe excel utilizzando EPPlus

Per esempio, se ho una classe:

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

Poi il seguente codice:

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(); 

... potrà aggiungere 2 righe da un foglio di lavoro chiamato "Clienti".

La mia domanda è se c'è una controparte conveniente per estrarre le righe da excel (ad esempio dopo aver apportato alcune modifiche) ai miei tipi.

Qualcosa di simile:

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

ho

  • stato alla ricerca attraverso il codebase EPPlus
  • cercato per eventuali saving domande
  • cercato per eventuali parsing domande
  • visto this domanda su readin g celle singole

... ma non ho trovato nulla su come analizzare semplicemente le righe sul mio tipo.

risposta

18

Ispirato da quanto sopra ho preso un percorso leggermente diverso.

  1. Ho creato un attributo e mappato ogni proprietà a una colonna.
  2. Io uso il tipo DTO per definire quello che mi aspetto ogni colonna di essere
  3. Consenti colonne non siano requried
  4. Usa EPPlus per convertire i tipi

In questo modo mi permette di uso tradizionale validazione dei modelli, e abbracciare i cambiamenti alle intestazioni delle colonne

- Uso:

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 che mappa per eccellere

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

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

Questa è la definizione di attributo

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


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

classe Extension per gestire file di mappatura per 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

Funziona alla grande. Nota per gli altri: 'const int RESOURCES_WORKSHEET = 1' (gli indici dei fogli di lavoro sono basati su 1) – fiat

+2

È semplice modificarlo in modo che l'attributo Column specifichi il Nome colonna invece dell'indice Colonna. – subsci

+0

Ero alla ricerca di un modo efficiente per mappare PropertyInfo e la Column in una lista, un buon uso di 'Func' e un buon uso di Linq, ho ridotto il dispendio di tempo per la stessa operazione nel 7.5% che non sembra molto, ma va da 680k ms a 50k ms Questa soluzione sarebbe più veloce se applicassi Parallel.foreach. Ho imparato molto da questa risposta. Grazie! – Nekeniehl

4

Purtroppo non esiste un metodo nativo per EPPlus. È un duro da spezzare perché dovresti usare i riflessi se vuoi davvero che sia generico. E poiché Excel memorizza tutti i numeri e le date in modo doppio, devi occuparti di un sacco di controlli unboxing e di tipo.

Questo è qualcosa su cui ho lavorato. È un metodo di estensione che lo farà tramite Generics. Funziona ma solo con test limitati quindi assicurati di controllarlo da solo. Non posso garantire che sia il più ottimizzato (ancora) ma è abbastanza decente al suo punto. Si potrebbe usare in questo modo:

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

L'estensione:

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; 
} 

un esempio completo:

[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; } 
} 

Console in uscita:

{ 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

Impressionante @Ernie, grazie per la condivisione. Effettivamente, alcune ipotesi, ma fornisce una grande intuizione su come risolvere questo. –