Open XML Dateien aufbauen und verändern (Teil 6)

Zeichenketten zu bestehenden Dateien hinzufügen

Bisher haben wir uns nur mit dem Neuerzeugen von Arbeitsmappen beschäftigt. Deshalb jetzt noch ein Blick hinter die Kulissen für den Fall, dass die Datei schon existiert.

private void AddSharedStringsToExistingWorksheet(string FName, string sheetName, string cellRef, string sharedString)
{
using (SpreadsheetDocument xlDoc = SpreadsheetDocument.Open(FName, true))
{
// Gibt es einen SharedStringTablePart?
// Nein ==> erzeugen

// Gibt es das Worksheet?
// Nein ==> erzeugen

// Gibt es die gesuchte Zeile?
// Nein ==> erzeugen

// Gibt es die gesuchte Zelle in der Zeile?
// Nein ==> erzeugen

// String einfügen
// Dokument speichern
}
}

Gibt es einen SharedStringTablePart?

Wir könnten jetzt natürlich wieder mit Inline Strings arbeiten (und es spricht eigentlich nichts dagegen), aber wir wollen streng “by the book” vorgehen die Zeichenketten in die Shared Strings-Tabelle eintragen. Dazu muss zuerst einmal geschaut werden, ob diese überhaupt vorhanden ist. Wenn nicht, muss sie erzeugt werden. GetPartsOfType() liefert eine Sammlung aller gefundenen Parts eines bestimmten Typs. Da es nur einen geben kann, können wir mit der Extension Method FirstOrDefault() die gewünschte Referenz erhalten. First() kann hier nicht verwendet werden, da nur FirstOrDefault() bei leerer Liste null liefert. In dem Fall müssen wir den Part selbst erzeugen.

SharedStringTablePart sstPart = xlDoc.WorkbookPart.GetPartsOfType<SharedStringTablePart>().FirstOrDefault();
if (sstPart == null)
sstPart = xlDoc.WorkbookPart.AddNewPart<SharedStringTablePart>();

Gibt es das Worksheet?

Zuerst müssen wir den WorkbookPart holen, da dort die Referenzen auf die Worksheets gehalten werden. Daraus läßt sich über Lambda Functions das Worksheet mit den gesuchten Namen ermitteln oder erzeugen, falls noch nicht vorhanden.

WorkbookPart wbPart = xlDoc.WorkbookPart; Workbook wb = wbPart.Workbook;
Sheet sheet = wb.GetFirstChild<Sheets>().Elements<Sheet>() .Where(s => s.Name == sheetName).FirstOrDefault();
WorksheetPart wsPart;
Worksheet ws = null;
if (sheet == null) // nicht da ==> erzeugen
{
wsPart = InsertWorksheetPart(wbPart, tbWorksheetName.Text);
ws = new Worksheet(new SheetData());
// worksheet.xml erzeugen
}

Wenn es schon existiert, könnten wir uns eigentlich zurücklehnen, da wir weiter ober ja schon das Sheet mit den entsprechenden Namen gefunden hatten. Weit gefehlt. Oben wird eine Referenz auf ein Sheet zurückgegeben, wir brauchen aber die Referenz auf das Worksheet. Das ist ein kleiner, aber feiner Unterschied. Über die ID des Sheets können wir den WorksheetPart extrahieren (GetPartById) und von dort kommen wir an das Worksheet.

else// es existiert
{
string rId = sheet.Id.Value;
wsPart = (WorksheetPart)xlDoc.WorkbookPart.GetPartById(rId); ws = wsPart.Worksheet;
}

Aus dem Worksheet muss nun noch das Root Element der Tabelle geholt werden - da wir ja vorher nicht wissen können, ob das Worksheet existiert.

SheetData sd = ws.GetFirstChild<SheetData>();

Gibt es die gesuchte Zeile?

Eine Excel-Tabelle ist zeilenweise aufgebaut. Unterhalb des SheetData Elements befinden sich die Tabellen-Zeilen im Markup. Also müssen wir die mit dem vorgegebenen Zeilenindex finden oder erzeugen. Die Hilfsfunktion RemoveAllButNumbers ist eine eigene Extension Method, die aus einer Zelladresse (AB123) nur die Zeilenreferenz (123) zurück liefert.

UInt32 rowIndex = (UInt32)cellRef.RemoveAllButNumbers();
Row row = sd.Elements<Row>() .Where(r => r.RowIndex.Value == rowIndex) .FirstOrDefault();
if (row == null)
row = CreateNewRow(ref sd, rowIndex);

Gibt es die gesuchte Zelle?

In etwa das Gleiche machen wir mit der Zelle innerhalb der eben gefundenen Zeile.

Cell cell = row.Elements<Cell>() .Where(c => c.CellReference == cellRef) .FirstOrDefault();
if (cell == null)
cell = CreateNewCell(ref row, cellRef);

Zellwerte schreiben und speichern

Zum Abschluß wird die Zeichenkette in der Shared Strings-Tabelle gespeichert bzw. - falls dort schon vorhanden - deren Index ermittelt und dieser in die ermittelte oder erzeugte Zelle eingetragen. (Die Routine zum Eintragen des Shared Strings wurde schon in Teil 2 beschrieben.)

cell.DataType = CellValues.SharedString; cell.CellReference = cellRef;
cell.CellValue = new CellValue(InsertSharedStringItem(sharedString, sstPart).ToString());
ws.Save(wsPart);

So, wie man sieht, kann man sich auf nichts verlassen ;-) und muss ständig prüfen, ob bestimmte Teile schon vorhanden sind. Es wird in der Praxis wohl weitaus häufiger vorkommen, schon existierende Dateien abfragen oder ändern zu müssen als neue zu erzeugen. Nichtsdestotrotz lohnt sich ein Blick auch hinter diese Kulissen, um die Zusammenhänge zu verstehen. So ein Dateiformat ist verdammt komplex und man wird als Einzelner wohl nie alles verstehen können, was in einer Excel- oder Word-Datei so alles vor sich geht. Das Gute an der Sache ist, das man nie alles braucht, sondern einzelne Teilbereiche relativ gut von anderen abgetrennt sind. So kommt man dann Schritt für Schritt voran.

Das Beispielprojekt kann übrigens hier heruntergeladen werden.

[Fortsetzung folgt]