Search code examples
c#exceldatasetopenxml-sdk

Hitting a wall attempting C# method using OpenXML SDK to export all the DataTables in a DataSet to be sheets in a new Excel Workbook


Here is a partial method to create a byte array that could be written to an xslx file that takes a DataSet and exports all the tables in that DataSet as sheets in an Excel workbook.... EXCEPT that all I have been able to do so far is get the sheets itself created, I can't for the life of me figure out how to populate those sheets with actual data:

    private byte[] DataSetToXlsx (DataSet ds)
    {
        byte[] bytes;

        using (var stream = new MemoryStream())
        {
            using (SpreadsheetDocument spreadsheet = SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook))
            {
                WorkbookPart workbookPart = spreadsheet.AddWorkbookPart();
                workbookPart.Workbook = new Workbook();

                WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
                worksheetPart.Worksheet = new Worksheet(new SheetData());  // This line needs to be here and I don't fully understand why

                Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets());

                for (int iTable = 0; iTable < ds.Tables.Count; iTable++)
                {
                    DataTable dt = ds.Tables[iTable];

                    var sheet = new Sheet
                    {
                        Id = workbookPart.GetIdOfPart(worksheetPart),
                        SheetId = (uint)iTable + 1,
                        Name = string.IsNullOrWhiteSpace(dt.TableName) ? $"Sheet {iTable + 1}" : dt.TableName
                    };

                    // Now I want to iterate through the DataRows on the table to transfer the values into the sheet, but I just can't figure out how to do it.

                    sheets.Append(sheet);
                }

                workbookPart.Workbook.Save();
            }
            stream.Seek(0, SeekOrigin.Begin);
            bytes = stream.ToArray();
        }

        return bytes;
    }

I realize that partly I don't fully grasp the difference between a Worksheet and a Sheet. And as my comments in the code would suggest, I don't fully understand WNY I need to create a single new Worksheet but one Sheet for every DataTable in the DataSet.

But my code does work to create a new workbook, and to put in that workbook a sheet for every DataTable.

But more to the point, I do not understand how to start creating rows in the Sheet instances so that the actual data itself gets reflected. I am just baffled at how to do this. Every attempt I have made compiles just fine but creates corrupted files that Excel can't read.

I would love to know the trick!

Please do not point me to third party libraries, or to Excel interrop.


Solution

  • I figured out the answer - here is my modified code:

        private byte[] DataSetToXlsx (DataSet ds)
        {
            if (ds == null || ds.Tables.Count < 1)
            {
                return null;
            }
    
            byte[] bytes;
    
            using (var stream = new MemoryStream())
            {
                using (SpreadsheetDocument spreadsheet = SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook))
                {
                    // Setup requirements for minimal spreadsheet document - workbook and sheets.
                    WorkbookPart workbookPart = spreadsheet.AddWorkbookPart();
                    workbookPart.Workbook = new Workbook();
                    Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets());
    
                    for (int iTable = 0; iTable < ds.Tables.Count; iTable++)
                    {
                        // Add a new worksheet part for the table
                        WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
                        worksheetPart.Worksheet = new Worksheet(new SheetData());
    
                        DataTable dt = ds.Tables[iTable];
    
                        // Add sheet to the workbook with ID from worksheet
                        sheets.Append(new Sheet
                        {
                            Id = workbookPart.GetIdOfPart(worksheetPart),
                            SheetId = (uint)iTable + 1,
                            Name = string.IsNullOrWhiteSpace(dt.TableName) ? $"Sheet {iTable + 1}" : dt.TableName
                        });
    
                        if (dt.Columns.Count < 1)
                        {
                            continue;  // No data in this table.
                        }
    
                        // Get the sheetData for the grid on which we will place rows.
                        SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();
    
                        CellValues[] columnTypes = new CellValues[dt.Columns.Count];
    
                        // Retrieve header data from the table columns and place in first row of the sheet
                        var row = new Row { RowIndex = 1 };
                        for (int iColumn = 0; iColumn < dt.Columns.Count; iColumn++)
                        {
                            DataColumn dc = dt.Columns[iColumn];
    
                            columnTypes[iColumn] = FindCellValuesForColumn(dc);
    
                            row.Append(new Cell { DataType = CellValues.String, CellValue = new CellValue(dc.ColumnName) });
                        }
                        sheetData?.Append(row);
    
                        // Retrieve remaining data from table rows and populate the sheet
                        for (int iRow = 0; iRow < dt.Rows.Count; iRow++)
                        {
                            DataRow dr = dt.Rows[iRow];
                            row = new Row { RowIndex = (uint)iRow + 2 };
                            for(int iColumn = 0; iColumn < dt.Columns.Count; iColumn++)
                            {
                                row.Append(new Cell { DataType = columnTypes[iColumn], CellValue = CellValueFromObject(dr[iColumn]) });
                            }
                            sheetData?.Append(row);
                        }
                    }
    
                    workbookPart.Workbook.Save();
                }
                stream.Seek(0, SeekOrigin.Begin);
                bytes = stream.ToArray();
            }
    
            return bytes;
        }
    

    This does depend on a couple of helper methods and a static lookup table which I have also included here:

        private static readonly IReadOnlyDictionary<Type, Tuple<CellValues, Func<object, CellValue>>> TypeLookup = new ReadOnlyDictionary<Type, Tuple<CellValues, Func<object, CellValue>>>(new Dictionary<Type, Tuple<CellValues, Func<object, CellValue>>>
        {
            { typeof(char), new Tuple<CellValues, Func<object, CellValue>>(CellValues.String, o => new CellValue(o.ToString())) },
            { typeof(string), new Tuple<CellValues, Func<object, CellValue>>(CellValues.String, o => new CellValue((string)o)) },
            { typeof(DateTime), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Date, o => new CellValue((DateTime)o)) },
            { typeof(bool), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Boolean, o => new CellValue((bool)o)) },
            { typeof(byte), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(Convert.ToInt32(o))) },
            { typeof(short), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(Convert.ToInt32(o))) },
            { typeof(int), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue((int)o)) },
            { typeof(long), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(o.ToString())) },
            { typeof(ushort), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(o.ToString())) },
            { typeof(uint), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(o.ToString())) },
            { typeof(ulong), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(o.ToString())) },
            { typeof(float), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue(Convert.ToDouble(o))) },
            { typeof(double), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue((double)o)) },
            { typeof(decimal), new Tuple<CellValues, Func<object, CellValue>>(CellValues.Number, o => new CellValue((decimal)o)) }
        });
    
        private static CellValue CellValueFromObject (object obj)
        {
            return obj == null
                ? new CellValue()
                : TypeLookup.TryGetValue(obj.GetType(), out Tuple<CellValues, Func<object, CellValue>> tuple)
                    ? tuple.Item2(obj)
                    : new CellValue(Convert.ToString(obj));
        }
    
        private static CellValues FindCellValuesForColumn(DataColumn dc)
        {
            dc = dc ?? throw new ArgumentNullException(nameof(dc));
    
            if (TypeLookup.TryGetValue(dc.DataType, out Tuple<CellValues, Func<object, CellValue>> tuple))
            {
                return tuple.Item1;
            }
    
            throw new InvalidOperationException($"No CellValues mapping available for data type \"{dc.DataType.FullName}\" for column \"{dc.ColumnName}\" in table \"{dc.Table?.TableName}\" in dataset \"{dc.Table?.DataSet?.DataSetName}\"");
        }