2015-10-30 26 views
10

EPPlus ma wygodną metodę LoadFromCollection<T>, aby pobrać dane z mojego własnego typu do arkusza roboczego.Jak przetworzyć wiersze Excela z powrotem na typy za pomocą EPPlus

Na przykład jeśli mam klasy:

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

Wtedy następujący kod:

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

... doda 2 rzędy na arkuszu o nazwie "Klienci".

Moje pytanie brzmi: czy istnieje wygodny odpowiednik do wyodrębnienia wierszy z programu Excel (na przykład po dokonaniu niektórych modyfikacji) z powrotem do moich typów.

Coś jak:

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

mam

  • zostały pominie kodzie EPPlus
  • poszukiwał wszelkich saving pytania
  • poszukiwali jakichkolwiek parsing pytania
  • widać this pytanie na readin g pojedynczych komórek

... ale nie znalazłem nic, jak po prostu przeanalizować wiersze według mojego typu.

Odpowiedz

18

Zainspirowany powyższą linią, wybrałem nieco inną trasę.

  1. Utworzono atrybut i zmapowałem każdą właściwość do kolumny.
  2. używam typu DTO zdefiniować czego oczekuję każda kolumna będzie
  3. Pozwól kolumny nie mogą być wymagane na
  4. Używaj EPPlus konwertować typy

Dzięki temu pozwala mi korzystać z tradycyjnych walidacji modelu, i objąć zmiany nagłówków kolumn

- Zastosowanie:

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 który odwzorowuje się doskonalić

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

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

Jest to definicja atrybutu

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


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

klasy Extension do obsługi wierszy mapowania 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

Działa świetnie. Uwaga dla innych: 'const int RESOURCES_WORKSHEET = 1' (indeksy skoroszytu są oparte na 1) – fiat

+2

Łatwo to zmienić, aby atrybut kolumny określał nazwę kolumny zamiast indeksu kolumny. – subsci

+0

Szukałem skutecznego sposobu mapowania PropertyInfo i kolumny na listę, miłego korzystania z 'Func' i miłego korzystania z Linq, zmniejszyłem czasochłonny dla tej samej operacji w 7,5%, co nie wydaje się dużo, ale przechodzi od 680k ms do 50k ms To rozwiązanie byłoby szybsze, gdyby zastosować Parallel.foreach. Wiele się nauczyłem od tej odpowiedzi. Dzięki! – Nekeniehl

4

Niestety, nie ma takiej metody macierzystej dla EPPlus. Jest to twardy orzech do zgryzienia, ponieważ będziesz musiał użyć odbić, jeśli naprawdę chcesz, aby był ogólny. A ponieważ Excel przechowuje wszystkie liczby i daty jako podwójne, musisz sobie poradzić z wieloma odpakowanymi i sprawdzonymi typami.

To jest coś, nad czym pracowałem. Jest to metoda rozszerzenia, która zrobi to poprzez Generics. Działa, ale tylko w przypadku ograniczonego testowania, więc upewnij się, sprawdź to sam. Nie mogę zagwarantować, że jest to najbardziej zoptymalizowany (jeszcze), ale w jego punkcie jest całkiem przyzwoity. Można by użyć go w ten sposób:

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

Rozszerzenie:

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

FULL przykład:

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

konsoli wyjściowa:

{ 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

Niesamowite @Ernie, dzięki za dzielenie się. Rzeczywiście, całkiem pewne założenia, ale daje doskonały wgląd w to, jak rozwiązać ten problem. –