Export to Excel using …

17. März 2015

Bei Anwendungen, die große Datenmengen verarbeiten, wird sehr häufig die Anforderung gestellt, die Daten in Excel bereit zu stellen, da die Möglichkeiten der Analyse oder der Weiterverarbeitung hier schneller und flexibler von statten gehen kann. Die Funktionalität des Excels in der Anwendung bereitzustellen ist auch nicht sinnvoll, obwohl mit Hilfe von Controls von Anbietern wie Telerik bereits sehr viel machbar ist. Nun stellt sich die Frage, welche Optionen habe ich, um Daten nach Excel zu exportieren.

Die nachfolgenden Beschreibungen und Beobachtungen basieren auf einer WPF Anwendung mit Office 2010. Als Optionen für den Export wurden betrachtet

  1. Excel Automation
  2. ClosedXML (Codeplex)
  3. EPPlus (Codeplex)
  4. Telerik RadGridView Control

Die kleine WPF-Anwendung hat lediglich ein DataGrid, in denen die zu exportierenden Daten angezeigt werden, eine Methode um eine bestimmte Anzahl an Daten zu laden und vier Methoden um die Daten nach Excel zu exportieren. Die Daten bestehen aus einer Liste von Objekten mit lediglich 4 Eigenschaften, die zufällig generiert werden. In allen vier Fällen wurden unterschiedliche Anzahlen an Datensätze exportiert und dabei die Laufzeit und der temporäre maximale zusätzliche Verbrauch an Arbeitsspeicher (Working Set (Memory)) beobachtet. Die Beobachtungen betrachten nur den ersten Aufruf des Exports.

image

Beim Export ist keine besondere Formatierung vorgenommen worden. Dies ist aber bei allen 4 Lösungen machbar. Ebenso wurden die Methoden gewählt, die den geringsten Aufwand hatten.

1. Option: Excel Automation

Microsoft stellt für seine Office-2010-Programme Primary Interop Assemblies (PIA) bereit, welche über ein Restributable Paket separat installiert werden können. Im Projekt muss nun eine Reference auf das Assembly „Microsoft.Office.Interop.Excel.dll“ aus dem Verzeichnis C:WindowsassemblyGAC_MSILMicrosoft.Office.Interop.Excel 14.0.0.0__71e9bce111e9429c gesetzt werden.

   1: public void SaveWithPIA(IEnumerable<BusinessItem> businessItems)

   2: {

   3:   var resultFile = @"c:	empExportPIA.xlsx";

   4:   if (File.Exists(resultFile))

   5:   {

   6:     File.Delete(resultFile);

   7:   }

   8:  

   9:   var application = new Microsoft.Office.Interop.Excel.Application();

  10:   application.Visible = false;

  11:   application.UserControl = false;

  12:   try

  13:   {

  14:     var workbook = application.Workbooks.Add();

  15:     var worksheet = workbook.ActiveSheet as Microsoft.Office.Interop.Excel.Worksheet;

  16:  

  17:     this.ExportItems(

  18:       worksheet, 

  19:       new List<string> { "ID", "Name", "UnitPrice", "Date" },

  20:       businessItems.ToList(), 

  21:       new List<Func<BusinessItem, object>>

  22:       {

  23:         p => p.ID, 

  24:         p => p.Name, 

  25:         p => p.UnitPrice,

  26:         p => p.Date,

  27:       });

  28:  

  29:     workbook.SaveAs(resultFile);

  30:   }

  31:   finally

  32:   {

  33:     application.Quit();

  34:     application = null;

  35:   }

  36: }

  37:  

  38: private void ExportItems<T>(

  39:   Microsoft.Office.Interop.Excel.Worksheet worksheet, 

  40:   List<string> headers,

  41:   List<T> items, 

  42:   List<Func<T, object>> valueGetters)

  43:    where T : class

  44: {

  45:   var headerArray = new object[1, headers.Count];

  46:  

  47:   for (int i = 0; i < headers.Count; i++)

  48:   {

  49:     headerArray[0, i] = headers[i];

  50:   }

  51:  

  52:   var leftUpperCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1, 1];

  53:   var rightLowerCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1, headers.Count];

  54:   var headerRow = worksheet.Range[leftUpperCell, rightLowerCell];

  55:   headerRow.Value = headerArray;

  56:   headerRow.Font.Bold = true;

  57:  

  58:   var dataArray = new object[items.Count, valueGetters.Count];

  59:  

  60:   for (var rowIndex = 0; rowIndex < items.Count; rowIndex++)

  61:   {

  62:     T item = items[rowIndex];

  63:     var columnIndex = 0;

  64:     foreach (var valueGetter in valueGetters)

  65:     {

  66:       dataArray[rowIndex, columnIndex] = valueGetter(item);

  67:       columnIndex++;

  68:     }

  69:   }

  70:  

  71:   leftUpperCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[2, 1];

  72:   rightLowerCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1 + items.Count, valueGetters.Count];

  73:   var dataRange = worksheet.Range[leftUpperCell, rightLowerCell];

  74:   dataRange.Value = dataArray;

  75: }

Vorteile

Bei größeren Datenmengen (100.000 Datensätze und mehr) ist diese Art des Exports mit annehmbarer Performance und auch Memory-Verbrauch verbunden. Die erzeugten Excel-Files sind von der Größe her mit einem „normalen“ Excel vergleichbar, was ja auch nicht verwunderlich ist, denn es wird ja Excel verwendet. Im Zuge eines MVVM-Pattern, kann der Export in einem ViewModel oder einer Service/Repository-Komponente implementiert werden.

Nachteile

Es ist auf dem System, wo der Export-Vorgang ausgeführt wird, die Installation von Excel notwendig. Dies kann bei einem Server gestütztem Vorgang eventuell Probleme bereiten, weil eine Office-Installation auf einem Server vom Betrieb nicht gerne gesehen wird. Ebenso muss die Liste der Objekte im Arbeitsspeicher „doppelt“ vorgehalten werden, da ein Objekt-Array erzeugt werden muss, um die Daten sinnvoll in die Excel-Zellen zu kopieren. Das Setzen des Wertes über eine einzelne Zelle verschlechtert die Performance erheblich.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs

Arbeitsspeicher + Excel (K)
5.000 +

100.000
5.000 +

100.000
7.000 +

100.000
40.000 +

100.000
430.000 +

410.000
Zeit (s) 3-4 3-4 3-4 4-6 20-22
Dateigröße (KB) 14 37 261 2.545 25.420

Für Excel 2013 sind hier weitere Informationen zu finden.

2. Option: ClosedXML

ClosedXML ist eine unter MIT License verfügbare Bibliothek auf Codeplex. Hier heißt es

„ClosedXML makes it easier for developers to create Excel 2007/2010 files. It provides a nice object oriented way to manipulate the files (similar to VBA) without dealing with the hassles of XML Documents. It can be used by any .NET language like C# and Visual Basic (VB).“

Sie basiert auf dem OpenXml Format und benötigt die Library DocumentFormat.OpenXml.dll (Download). Nach der Referenzierung der beiden DLL’s „ClosedXML.dll“und „DocumentFormat.OpenXml.dll“ kann eine Liste von Objekten wie im Listing zu sehen gespeichert werden.

   1: public void SaveWithOpenXml(IEnumerable<BusinessItem> businessItems)

   2: {

   3:   var strResultFile = @"c:	empExportClosedXml.xlsx";

   4:   if (File.Exists(strResultFile))

   5:   {

   6:     File.Delete(strResultFile);

   7:   }

   8:  

   9:   using (var workbook = new XLWorkbook(XLEventTracking.Disabled))

  10:   {

  11:     using (var worksheet = workbook.Worksheets.Add("ExportData"))

  12:     {

  13:       worksheet.Cell(1, 1).InsertTable(businessItems);

  14:     }

  15:  

  16:     workbook.SaveAs(strResultFile);

  17:   }

  18: }

Vorteile

Es ist keine Excel-Installation auf dem ausführenden System notwendig. Die erzeugten Excel-Files sind von der Größe her mit einem „normalen“ Excel vergleichbar. Im Zuge eines MVVM-Pattern, kann der Export in einem ViewModel oder einer Service/Repository-Komponente implementiert werden. Ebenso ist eine akzeptable Performance bei kleineren Datenmengen zu beobachten.

Nachteile

Der zusätzliche Bedarf an Arbeitsspeicher bei größeren Datenmengen ist enorm.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs

Arbeitsspeicher (K)
12.000 24.000 50.000 520.000 4.700.000
Zeit (s) 1-2 1-2 2-3 8-10 90-110
Dateigröße (KB) 11 30 218 2.114 21.091

3. Option: EPPlus

EPPlus ist eine unter GNU Library General Public License verfügbare Bibliothek auf Codeplex. Hier heißt es

„EPPlus is a .net library that reads and writes Excel 2007/2010 files using the Open Office Xml format (xlsx).“

Nach der Referenzierung der DLL „EPPlus.dll“ kann eine Liste von Objekten wie im Listing zu sehen gespeichert werden.

   1: public void SaveWithEpplus(IEnumerable<BusinessItem> businessItems)

   2: {

   3:   var strResultFile = @"c:	empExportEpplus.xlsx";

   4:   if (File.Exists(strResultFile))

   5:   {

   6:     File.Delete(strResultFile);

   7:   }

   8:  

   9:   var fileInfo = new FileInfo(strResultFile);

  10:  

  11:   using (var excelPackage = new ExcelPackage(fileInfo))

  12:   {

  13:     var worksheet = excelPackage.Workbook.Worksheets.Add("ExportData");

  14:     

  15:     worksheet.Cells.LoadFromCollection(businessItems, true);

  16:  

  17:     excelPackage.Save();

  18:   }

  19: }

Vorteile

Es ist keine Excel-Installation auf dem ausführenden System notwendig. Die erzeugten Excel-Files liegen von der Größe her leicht unter dem „normalen“ Excel. Im Zuge eines MVVM-Pattern, kann der Export in einem ViewModel oder einer Service/Repository-Komponente implementiert werden. Sehr performant bei kleineren und mittleren Datenmengen, sehr sparsam im Arbeitsspeicherverbrauch.

Nachteile

Die aktuellste Version (4.0.2) hat beim Speichern eine Exception geworfen, deswegen wurde im Test die Version 3.1 verwendet. Das Datumsfeld wurde im Excel nicht als Datum formatiert, sondern es steht dort der numerische Wert.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs

Arbeitsspeicher (K)
4.000 5.000 10.000 105.000 780.000
Zeit (s) <1 <1 <1 6-7 60-70
Dateigröße (KB) 5 23 197 1.986 19.819

4. Option: Telerik RadGridView

Telerik ist einer der professionellen Anbieter von Controls um Anwendungen mit mehr User-Expierence auszustatten. Das RadGridView bietet eine Export Funktion an, die es auch ermöglicht Excel-kompatible Dateien zu erzeugen.

Eine Code-Behind Methode könnte dann so aussehen

   1: private void ExportWithTelerik(object sender, RoutedEventArgs e)

   2: {

   3:   try

   4:   {

   5:     using (var stream = File.OpenWrite(@"c:	empExportTelerik.xls"))

   6:     {

   7:       var exportOptions = new GridViewExportOptions();

   8:       exportOptions.Format = ExportFormat.ExcelML;

   9:       exportOptions.ShowColumnFooters = false;

  10:       exportOptions.ShowColumnHeaders = true;

  11:       exportOptions.ShowGroupFooters = false;

  12:       exportOptions.Encoding = Encoding.UTF8;

  13:  

  14:       ExportGridView.Export(stream, exportOptions);

  15:     }

  16:   }

  17:   catch (Exception ex)

  18:   {

  19:     TBMessage.Text = ex.Message;

  20:   }

  21: }

Vorteile

Es wird die Ansicht nahezu 1:1 auch in Excel angezeigt. Dies betrifft auch eventuell vorhandene Gruppierungen und Aggregationsfunktionen. Der Arbeitsspeicherverbrauch ist gering, da die Daten ja bereits im DataGrid vorliegen und somit nicht nochmal aufbereitet werden müssen und auch XML erzeugt wird. Gute Performance auch bei größeren Datenmengen.

Nachteile

Das Speichern erfolgt in einem Excel-kompatiblen XML Format. Es erscheint beim Öffnen der Datei immer die Warnung, dass das Format nicht korrekt vorliegt. Excel ist aber in der Lage dann die Daten anzuzeigen. Da XML abgespeichert wird, ist die Dateigröße entsprechend groß. Im Zuge eines MVVM Pattern ist dies weniger zu gebrauchen, da die Methodik am Control RadGridView vorhanden ist. Es sollte dann schon im Code-Behind des UserControls implementiert werden.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs

Arbeitsspeicher (K)
1.000 1.000 1.000 1.000 10.000
Zeit (s) <1 <1 <1 2-3 22-24
Dateigröße (KB) 28 271 2.710 27.182 272.785

Fazit

Für eine Export-Methode, die dem MVVM-Pattern möglichst nahe kommt, ist eine der ersten 3 Optionen die richtige Wahl. Ist der Export nur auf dem Client notwendig und nicht Server Szenarien zu berücksichtigen würde ich die PIA Option wählen. In einem Server-Szenario ist sicherlich eine Excel-freie Option, die bessere Wahl. Hier scheint EPPlus eine gute Option zu sein, jedoch habe ich bisher keine näheren Erfahrungen mit dieser Bibliothek gemacht, da ich während des Test zufällig auf diese gestoßen bin. Kommt ein „SnapShot“ der angezeigten Daten am ehesten der fachlichen Anforderung nahe und man kann zum Beispiel die Telerik Controls im Projekt nutzen, so diese Option eine für mich zu favorisierende Wahl.