Code Objective
I need to create a System.Data.DataSet
object from an Excel workbook. Each DataTable
within the DataSet
must correspond to a nonempty worksheet in the workbook. The top row of each sheet will be used as column names for each DataTable
and the columns will be populated as the string representation of the worksheet data. The string part here is very important--everything in the final DataSet
must be visually identical to what is in the Excel file with no assumptions about data types.
The Issue
My code is horrendously slow. The workbook I'm using to test my code has one worksheet which uses the Excel columns A through CO and uses rows 1 through 11361, so I don't expect it to be too blazing fast, but I finally stopped it after 20 minutes of letting it run. That's really slow, even for a big workbook.
My Goal Here
I would love your help in determining the cause of the slowness, or perhaps if there's an altogether more efficient way of going about this. My instinct was to post this to Stack Overflow, but I figured this would be a better place because the code actually works and does what I want it to do, it just does it very slowly.
The Code
I used Dylan Morley's code here for creating an Excel "wrapper" which aids with orphaned Excel processes and the release of COM objects.
namespace Excel.Helpers
{
internal class ExcelWrapper : IDisposable
{
private class Window
{
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd,
out IntPtr ProcessID);
public static IntPtr GetWindowThreadProcessId(IntPtr hWnd)
{
IntPtr processID;
IntPtr returnResult = GetWindowThreadProcessId(hWnd, out processID);
return processID;
}
public static IntPtr FindExcel(string caption)
{
IntPtr hWnd = FindWindow("XLMAIN", caption);
return hWnd;
}
}
private Application excel;
private IntPtr windowHandle;
private IntPtr processID;
private const string ExcelWindowCaption = "MyUniqueExcelCaption";
public ExcelWrapper()
{
excel = CreateExcelApplication();
windowHandle = Window.FindExcel(ExcelWindowCaption);
processID = Window.GetWindowThreadProcessId(windowHandle);
}
private Application CreateExcelApplication()
{
Application excel = new Application();
excel.Caption = ExcelWindowCaption;
excel.Visible = false;
excel.DisplayAlerts = false;
excel.AlertBeforeOverwriting = false;
excel.AskToUpdateLinks = false;
return excel;
}
public Application Excel
{
get { return this.excel; }
}
public int ProcessID
{
get { return this.processID.ToInt32(); }
}
public int WindowHandle
{
get { return this.windowHandle.ToInt32(); }
}
public void Dispose()
{
if (excel != null)
{
excel.Workbooks.Close();
excel.Quit();
Marshal.ReleaseComObject(excel);
excel = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
try
{
Process process = Process.GetProcessById(this.ProcessID);
if (process != null)
{
process.Kill();
}
}
catch
{
throw;
}
}
}
}
}
Now I make a class that processes the Excel files using Microsoft.Office.Interop.Excel
operations.
namespace FileProcessing
{
class FileToProcess
{
public string File { get; set; }
public DataSet GetExcelData()
{
using (var wrapper = new Excel.Helpers.ExcelWrapper())
{
Application oXL = wrapper.Excel;
Workbook oWB = oXL.Workbooks.Open(this.File, 0, true);
try
{
DataSet excelData = new DataSet();
foreach (Worksheet oSheet in oWB.Worksheets)
{
int nColumns = oSheet.UsedRange.Columns.Count;
int nRows = oSheet.UsedRange.Rows.Count;
if (nColumns > 0)
{
System.Data.DataTable sheetData = excelData.Tables.Add(oSheet.Name);
Range rHeaders = (Range)oSheet.UsedRange.Rows[1];
for (int j = 1; j <= nColumns; j++)
{
string columnName = rHeaders.Columns[j].Text.ToString();
if (string.IsNullOrEmpty(columnName))
continue;
else if (sheetData.Columns.Contains(columnName))
{
int i = 1;
string c;
do
{
c = columnName + i.ToString();
i += 1;
}
while (sheetData.Columns.Contains(c));
sheetData.Columns.Add(c, typeof(string));
}
else
sheetData.Columns.Add(columnName, typeof(string));
}
for (int i = 2; i <= nRows; i++)
{
DataRow sheetRow = sheetData.NewRow();
for (int j = 1; j <= nColumns; j++)
{
string columnName = rHeaders.Columns[j].Text.ToString();
Range oRange = (Range)oSheet.Cells[i, j];
if (!string.IsNullOrEmpty(columnName))
sheetRow[columnName] = oRange.Text.ToString();
}
sheetData.Rows.Add(sheetRow);
}
}
}
return excelData;
}
catch (Exception)
{
throw;
}
finally
{
oWB.Close();
oWB = null;
oXL.Quit();
oXL = null;
wrapper.Dispose();
}
}
}
}
Finally I try to use the class. This is just a testing example where I record the amount of time the code takes to read the file and construct the dataset.
class Program
{
static void Main(string[] args)
{
Stopwatch sw = Stopwatch.StartNew();
FileToProcess testXLFile = new FileToProcess();
testXLFile.File = @"C:\BigWorkbook.xls";
DataSet ds = testXLFile.GetExcelData();
sw.Stop();
TimeSpan ts = sw.Elapsed;
Console.WriteLine("Elapsed Time: {0}ms", ts.Milliseconds);
Console.ReadKey();
}
}
}
Closing Remarks
My thought here is that reading each cell one at a time is taking forever on huge worksheets. I don't know how else to ensure that each value is properly captured as a string though. I don't want to ever assume that a particular column or cell has a particular data type to avoid conversion issues and preserve data integrity between the Excel file and the DataSet
. I saw user3488442's question here but I'm not convinced that I could modify it to suit my needs without running into conversion issues. Conversion between dates (which import as DateTime
objects in C#) and strings doesn't always preserve the display format in the worksheet. I also saw this question but it uses a VBA macro (and I don't speak German). Any input would be much appreciated.