Zysce
Zysce

Reputation: 1240

Using Open Xml to create xlsx file for Excel 2013

I need to create an Excel file xlsx. So, i decided to use Open xml (Open Xml 2.0 because i have to use .net 3.5).

After some research about open xml, i created this class :

public class ExcelFileManager : FileManager
{
    private SpreadsheetDocument _package;

    private Row _currentRow;
    private SheetData _sheetData;

    private int? _cellColumnN2;
    private int _cellColumnN1;
    private int _cellRow;

    public ExcelFileManager(string path) 
        : base(path)
    {
        _package = SpreadsheetDocument.Create(path, DocumentFormat.OpenXml.SpreadsheetDocumentType.Workbook);

        _cellColumnN1 = 65;
        _cellRow = 1;

        CreateSpreadSheet();
    }

    private void CreateSpreadSheet()
    {
        var workbookPart1 = _package.AddWorkbookPart();

        workbookPart1.Workbook = new Workbook();
        var worksheetPart1 = workbookPart1.AddNewPart<WorksheetPart>();
        worksheetPart1.Worksheet = new Worksheet();

        var sheets = _package.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
        var sheet = new Sheet()
        {
            Id = _package.WorkbookPart.
                GetIdOfPart(worksheetPart1),
            SheetId = 1,
            Name = "Users"
        };
        sheets.Append(sheet);

        _sheetData = new SheetData();
        _currentRow = new Row();

        _sheetData.Append(_currentRow);
        worksheetPart1.Worksheet.Append(_sheetData);
    }

    public override void AddHeader(IEnumerable<string> columnsName)
    {
        if (columnsName == null || !columnsName.Any())
            throw new ArgumentNullException("columnsName");

        try
        {
            AddRow(columnsName.ToArray());
        }
        catch (Exception e)
        {
            Tracing.Application.TraceError(-1, "An error occured when adding header to excel file", e);

            throw;
        }
    }

    public override void AddRow(params object[] values)
    {
        if (values == null || !values.Any())
            throw new ArgumentNullException("values");

        try
        {
            var listCells = new List<OpenXmlElement>();

            foreach (var item in values)
            {
                listCells.Add(CreateCellRow(item));
            }

            _currentRow.Append(listCells);

            AddNewRow();
        }
        catch (Exception e)
        {
            Tracing.Application.TraceError(-1, "An error occured when adding row to excel file", e);

            throw;
        }
    }

    public override void AddCell(object value)
    {
        try
        {
            var cell = CreateCellRow(value);

            _currentRow.Append(cell);
        }
        catch (Exception e)
        {
            Tracing.Application.TraceError(-1, "An error occured when adding cell to excel file", e);

            throw;
        }
    }

    public override void AddNewRow()
    {
        _currentRow = new Row();

        _sheetData.Append(_currentRow);

        IncreaseRow();
    }

    #region Utils
    private Cell CreateCellRow(object value)
    {
        Tracing.Application.TraceInformation("Add value \"{0}\" in cell {1}", value, GetCellLocation());

        Cell cell = new Cell();

        if (value == null)
        {
            cell.CellValue = new CellValue(string.Empty);
            cell.DataType = CellValues.String;
        }
        else
        {                
            cell.CellValue = new CellValue(value.ToString());
            cell.DataType = GetCellType(value); 
        }

        cell.CellReference = GetCellLocation();

        IncreaseCellColumn();

        return cell;
    }

    private CellValues GetCellType(object value)
    {
        CellValues type = CellValues.String;

        if (value.GetType() == typeof(DateTime) || value.GetType() == typeof(DateTime?))
            type = CellValues.Date;

        if (value.GetType() == typeof(bool) || value.GetType() == typeof(bool?))
            type = CellValues.Boolean;

        if (IsNumericType(value.GetType()))
            type = CellValues.Number;

        return type;
    }

    public bool IsNumericType(Type type)
    {
        TypeCode typeCode = Type.GetTypeCode(type);

        //The TypeCode of numerical types are between SByte (5) and Decimal (15).
        return (int)typeCode >= 5 && (int)typeCode <= 15;
    }

    private void IncreaseCellColumn()
    {
        if (_cellColumnN1 < 90)
            _cellColumnN1++;
        else
        {
            _cellColumnN1 = 65;

            if (!_cellColumnN2.HasValue)
                _cellColumnN2 = 65;
            else
                _cellColumnN2++;
        }
    }

    private void IncreaseRow()
    {
        _cellRow++;
        _cellColumnN1 = 65;
        _cellColumnN2 = null;
    }

    private string GetCellLocation()
    {
        Char? c1 = null;

        if (_cellColumnN2.HasValue)
            c1 = (char)_cellColumnN2;

        Char c2 = (char)_cellColumnN1;

        return string.Format("{0}{1}{2}", c1, c2, _cellRow);
    }
    #endregion

    #region IDisposable
    public override void Dispose()
    {
        if (_package != null)
        {
            _package.WorkbookPart.Workbook.Save();
            _package.Close();
        }
    }
    #endregion
}

When i create an excel file using this class, an error message is displayed : "We found an error in the content..." Excel repairs the file and it become openable. Why do i have this error in Excel ?

I looked a bit in the xml file sheet.xml contained in the xlsx file, there are few differences :

Generated file :

<x:worksheet xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <x:sheetData>
    <x:row>
      <x:c r="A1" t="str">
        <x:v>a string</x:v>
      </x:c>
      <x:c r="B1" t="str">
        <x:v>another string</x:v>
      </x:c>
    </x:row>
    ...
  </x:sheetData>
</x:worksheet>

Repaired file :

<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">
  <dimension ref="A1:S12"/>
  <sheetViews>
    <sheetView tabSelected="1" workbookViewId="0">
      <selection activeCell="I11" sqref="I11"/>
    </sheetView>
  </sheetViews>
  <sheetFormatPr baseColWidth="10" defaultRowHeight="15" x14ac:dyDescent="0.25"/>
  <sheetData>
    <row r="1" spans="1:19" x14ac:dyDescent="0.25">
      <c r="A1" t="s">
        <v>0</v>
      </c>
      <c r="B1" t="s">
        <v>1</v>
      </c>
      <c r="C1" t="s">
        <v>2</v>
      </c>
   </row>
   ...
 </sheetData>
 <pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
</worksheet>

I tried to change namespaces, but it didn't work. I know that shared strings are used in the second xml, but i don't think it's the problem. ^^ I don't care about style like page margin, font,... (may be auto adjust width for columns would be useful ^^)

I just want a well formed xlsx file.

Do i have a mistake or an omission in my code ?

Sorry for my english by the way '^^

Thanks.

Upvotes: 0

Views: 2256

Answers (1)

Markus Jarderot
Markus Jarderot

Reputation: 89231

Dates are stored as numbers with type number, but with a style applied to them. Booleans are also stored as numbers, but with type boolean.

private Cell CreateCellRow(object value)
{
    Cell cell = new Cell();
    cell.CellReference = GetCellLocation();
    IncreaseCellColumn();

    if (value == null)
    {
        cell.DataType = CellValues.InlineString;
        cell.InlineString = new InlineString
        {
            Text = new Text("")
        };
        return cell;
    }

    switch (Type.GetTypeCode(value.GetType()))
    {
        case TypeCode.DateTime:
            cell.StyleIndex = 1;
            cell.CellValue = new CellValue(((DateTime)value).ToOADate().ToString(CultureInfo.InvariantCulture));
            break;
        case TypeCode.String:
            cell.DataType = CellValues.InlineString;
            cell.InlineString = new InlineString {Text = new Text((string) value)};
            break;
        case TypeCode.Boolean:
            cell.DataType = CellValues.Boolean;
            cell.CellValue = new CellValue((bool)value ? "1" : "0");
            break;
        case TypeCode.Byte:
        case TypeCode.Int16:
        case TypeCode.Int32:
        case TypeCode.Int64:
        case TypeCode.SByte:
        case TypeCode.UInt16:
        case TypeCode.UInt32:
        case TypeCode.UInt64:
        case TypeCode.Decimal:
        case TypeCode.Double:
        case TypeCode.Single:
            cell.CellValue = new CellValue(((IFormattable)value).ToString(null, CultureInfo.InvariantCulture));
            break;
        default:
            throw new Exception("Unrecognized type: " + value.GetType().FullName);
    }

    return cell;
}

To create the style needed (snipped from another answer):

var stylesPart = workbookPart.AddNewPart<WorkbookStylesPart>();
stylesPart.Stylesheet = CreateStylesheet();
private Stylesheet CreateStylesheet()
{
    var stylesheet = new Stylesheet();

    // Default Font
    var fonts = new Fonts() { Count = 1, KnownFonts = BooleanValue.FromBoolean(true) };
    var font = new Font
    {
        FontSize = new FontSize() { Val = 11 },
        FontName = new FontName() { Val = "Calibri" },
        FontFamilyNumbering = new FontFamilyNumbering() { Val = 2 },
        FontScheme = new FontScheme() { Val = new EnumValue<FontSchemeValues>(FontSchemeValues.Minor) }
    };
    fonts.Append(font);
    stylesheet.Append(fonts);

    // Default Fill
    var fills = new Fills() { Count = 1 };
    var fill = new Fill();
    fill.PatternFill = new PatternFill() { PatternType = new EnumValue<PatternValues>(PatternValues.None) };
    fills.Append(fill);
    stylesheet.Append(fills);

    // Default Border
    var borders = new Borders() { Count = 1 };
    var border = new Border
    {
        LeftBorder = new LeftBorder(),
        RightBorder = new RightBorder(),
        TopBorder = new TopBorder(),
        BottomBorder = new BottomBorder(),
        DiagonalBorder = new DiagonalBorder()
    };
    borders.Append(border);
    stylesheet.Append(borders);

    // Default cell format and a date cell format
    var cellFormats = new CellFormats() { Count = 2 };

    var cellFormatDefault = new CellFormat { NumberFormatId = 0, FormatId = 0, FontId = 0, BorderId = 0, FillId = 0 };
    cellFormats.Append(cellFormatDefault);

    var cellFormatDate = new CellFormat { NumberFormatId = 22, FormatId = 0, FontId = 0, BorderId = 0, FillId = 0, ApplyNumberFormat = BooleanValue.FromBoolean(true) };
    cellFormats.Append(cellFormatDate);

    stylesheet.Append(cellFormats);

    return stylesheet;
}

Upvotes: 1

Related Questions