Template Based Approach to Export Data to Excel: Part - II

In this article we will learn how to add columns dynamically in a Excel Sheet using a Template based approach.

Overview

This is the continuation of my article Template based approach to export data to Excel – Part I .The basic idea of the demonstration is that we have a pre-defined template for the data and we want to write in the cell using the Open XML SDK. The template and the sample project will be uploaded in my last article. Let's have some overview of that first. We need to understand the concept first and we can compare the code.

Purpose

Suppose we have 8 properties of our model class. The last 3 properties are dynamic. The class is bound to a grid that we want to export. But the problem arises that sometimes all 8 properties are filled and sometimes only 5 properties will have data. The grid displays only 5 columns. The Excel template already has a Table part defined in Sheet 1 mapped to those 5 columns. Suppose we have a condition where the other three fields have data inside it. Then we need to add dynamic columns to the Excel table part. The basic purpose then if number of columns having data > 5 then append columns in the Excel template.

Also, our template contains 2 extra sheets that are related to the first sheet. That's where a relationship is useful. The data shown in the grid can be related to other tables in the database. What if the management wants to export all the related data? This cannot be done by a simple grid export.

Add columns dynamically

  1. First we need to add the following namespaces to the files:
    1. using DocumentFormat.OpenXml;  
    2. using DocumentFormat.OpenXml.Packaging;  
    3. using DocumentFormat.OpenXml.Spreadsheet;  
  2. Include the template Excel in your project and then open it in your code by:
    1. SpreadsheetDocument.Open(outputFilePath, true)  
  3. The SDK has classes for a Workbook, Sheet and Table related to the XML file structure I tried to explain in the previous article. We need to traverse it like we traverse when we work with simple XML. But we don't need to do LINQ to XML here as they are simplified.
    1. workbookPart = excel.WorkbookPart; //workbook part  
    2. currWorksheet = GetCurrentSheet(workbookPart,"Tender Details"); //Get "Tender Details sheet  
    3. currWorksheetPart = workbookPart.GetPartById(currWorksheet.Id.Value) as WorksheetPart; //Get current work sheet based on reference Id  
    4. sheetdata = currWorksheetPart.Worksheet.GetFirstChild<SheetData>(); // Get sheetdata  
    5.   
    6. foreach (var part in currWorksheetPart.TableDefinitionParts)  
    7. {  
    8.     if (part.Table.Name == "ItemTable")  
    9.     {  
    10.         table = part.Table;  
    11.         newTablePartId = currWorksheetPart.GetIdOfPart(part);  
    12.         break;  
    13.     }  
    14. }  
    You can see that we went to our table part named “ItemTable” in the template. It is present in Sheet1 named “Tender Details”. I hope I named the fields so they can be self-descriptive.

  4. Now if the requirement is to add dynamic columns, then we need to first identify what the starting cell no is for the table. Then we need to find the end cell number for the table. We need to get the Row details to add a new row dynamically.
    1. range = table.Reference.Value.Split(':');  
    2. startColumn = Utility.GetColumnName(range[0]);  
    3. endColumn = Utility.GetColumnName(range[1]);  
    4. startRowTableRange = Utility.GetRowIndex(range[0]);  
    5. endRowTableRange = Utility.GetRowIndex(range[1]);  
    6.   
    7. var anchorRow = sheetdata.Elements<Row>().Where(row => row.RowIndex == startRowTableRange).First();  
    8. var blankanchorRow = sheetdata.Elements<Row>().Where(row => row.RowIndex == endRowTableRange).First();  
    9. var anchorCell = sheetdata.Elements<Row>().Where(r => r.RowIndex == 2).First().Elements<Cell>().Where(c => string.Compare(c.CellReference.Value, "A2"true) == 0).First();  
    10. templateCells = blankanchorRow.Elements<Cell>().ToList();  
    11. var blankanchorCell = templateCells.Last();   
    Template cells are very important here because we will write data into these cells in the future. Anchor cells are used to get the cell-template and styles.

  5. Now when we get all the basic details about a row and cell we can add columns dynamically. All we need to do is create a new cell and append it to the last column of the table. And we need to add it to the templateCells collection since we need to write data in the future.
    1. newColumn.Id = (uint)(totalColumns + i);  
    2. newColumn.DataFormatId = lastcolumn.DataFormatId;  
    3. newColumn.Name = colName;  
    4. newColumn.DataCellStyle = lastcolumn.DataCellStyle;  
    5. newColumnList.Add(newColumn);  
    6.   
    7. endColumn = Utility.GetNextColumnName(endColumn); //Get the next column  
    8.   
    9. var index = InsertSharedStringItem(workbookPart, colName);//insert shared string item  
    10.   
    11. var newCell = new Cell();  
    12. newCell.CellReference = string.Format("{0}{1}", endColumn, startRowTableRange);  
    13. newCell.StyleIndex = anchorCell.StyleIndex;  
    14. newCell.CellValue = new CellValue(index.ToString());  
    15. newCell.DataType = new EnumValue<CellValues>(CellValues.SharedString);  
    16.   
    17. anchorRow.Append(newCell);  
    18. //add blank row   
    19. index = InsertSharedStringItem(workbookPart, colName.Replace(" ",""));  
    20. newCell = new Cell();  
    21. newCell.CellReference = string.Format("{0}{1}", endColumn, endRowTableRange); ;  
    22. newCell.StyleIndex = blankanchorCell.StyleIndex;  
    23. newCell.CellValue = new CellValue(index.ToString());  
    24. newCell.DataType = new EnumValue<CellValues>(CellValues.SharedString);  
    25.   
    26. blankanchorRow.Append(newCell);  
    Also we need to update the Shared string collection as shared strings only to maintain the reference of columns:
    1. var stringTable = stringTablePart.SharedStringTable;  
    2. if (stringTable == null)  
    3. {  
    4.     stringTable = new SharedStringTable();  
    5. }  
    6.   
    7. // Iterate through all the items in the SharedStringTable.   
    8. // If the text already exists, return its index.  
    9. foreach (SharedStringItem item in stringTable.Elements<SharedStringItem>())  
    10. {  
    11.     if (item.InnerText == value)  
    12.     {  
    13.         found = true;  
    14.         break;  
    15.     }  
    16.     index += 1;  
    17. }  
    18.   
    19. if (!found)  
    20. {  
    21.     stringTable.AppendChild(new SharedStringItem(new Text(value)));  
    22.     stringTable.Save(); //Save the string in shared string  
    23. }  
  6. The last step is to update the table's column collections and then save the workbook. We also need to update the table reference since we already changed the table definition.
    1. table.TableColumns.Append(newColumnList.ToArray());  
    2. table.TableColumns.Count = (uint)(totalColumns + columnsToAdd);  
    3.   
    4. if (totalRecords == 0)  
    5. {  
    6.     table.Reference = string.Format("{0}{1}:{2}{3}", startColumn, startRowTableRange, endColumn, endRowTableRange);  
    7. }  
    8. else  
    9. {  
    10.     table.Reference = string.Format("{0}{1}:{2}{3}", startColumn, startRowTableRange, endColumn, endRowTableRange + totalRecords - 1);  
    11. }  
    12. table.Save();  
    13.   
    14. //append all rows  
    15. templateCells = blankanchorRow.Elements<Cell>();  
    16.   
    17. //delete anchor row  
    18. sheetdata.RemoveChild(blankanchorRow);  
    19.   
    20. //save worksheet  
    21. currWorksheetPart.Worksheet.Save();  

So in this article we saw how to add columns dynamically. In the next article I will show you the various ways to write data in the cells.