views:

767

answers:

5

This was working..and I moved the disposal code to the finally block, and now it fails every time.

I have a test spreadsheet with 4 records, 6 columns long. Here is the code I'm using to bring it in. This is ASP .Net 3.5 on IIS 5 (my pc) and on IIS 6 (web server).

It blows up on the line right before the catch: "values = (object[,])range.Value2;" with the following error:

11/2/2009 8:47:43 AM :: Not enough storage is available to complete this operation. (Exception from HRESULT: 0x8007000E (E_OUTOFMEMORY))

Any ideas? Suggestions? I got most of this code off codeproject, so I have no idea if this is the correct way to work with Excel. Thanks for any help you can provide.

Here is my code:

Excel.ApplicationClass app = null;
Excel.Workbook book = null;
Excel.Worksheet sheet = null;
Excel.Range range = null;

object[,] values = null;

try
{
    // Configure Excel
    app = new Excel.ApplicationClass();
    app.Visible = false;
    app.ScreenUpdating = false;
    app.DisplayAlerts = false;

    // Open a new instance of excel with the uploaded file
    book = app.Workbooks.Open(path);

    // Get first worksheet in book
    sheet = (Excel.Worksheet)book.Worksheets[1];

    // Start with first cell on second row
    range = sheet.get_Range("A2", Missing.Value);

    // Get all cells to the right
    range = range.get_End(Excel.XlDirection.xlToRight);

    // Get all cells downwards
    range = range.get_End(Excel.XlDirection.xlDown);

    // Get address of bottom rightmost cell
    string downAddress = range.get_Address(false, false, Excel.XlReferenceStyle.xlA1, Type.Missing, Type.Missing);

    // Get complete range of data
    range = sheet.get_Range("A2", downAddress);

    // get 2d array of all data
    values = (object[,])range.Value2;
}
catch (Exception e)
{
    LoggingService.log(e.Message);
}
finally
{
    // Clean up
    range = null;
    sheet = null;

    if (book != null)
        book.Close(false, Missing.Value, Missing.Value);

    book = null;

    if (app != null)
        app.Quit();

    app = null;
}

return values;
+3  A: 

I'm not sure if this is your issue or not, but it very well may be. You are not cleaning up your excel objects properly. They are unmanaged code and can be tricky to clean up. Finally should look something like this: And as the comments have noted working with excel from asp.net is not a good idea. This cleanup code is from a winform app:

 GC.Collect();
 GC.WaitForPendingFinalizers();


 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(range);
 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(sheet);
 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(book);

 WB.Close(false, Type.Missing, Type.Missing);

 Excel.Quit();
 System.Runtime.InteropServices.Marshal.FinalReleaseComObject(Excel);

EDIT

An alternative would be to use ado.net to open the workbook.

            DataTable dt = new DataTable();

            string connectionString;
            System.Data.OleDb.OleDbConnection excelConnection;
            System.Data.OleDb.OleDbDataAdapter da;
            DataTable dbSchema;
            string firstSheetName;
            string strSQL;

            connectionString = @"provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + @";Extended Properties=""Excel 12.0;HDR=YES;IMEX=1""";
            excelConnection = new System.Data.OleDb.OleDbConnection(connectionString);
            excelConnection.Open();
            dbSchema = excelConnection.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, null);
            firstSheetName = dbSchema.Rows[0]["TABLE_NAME"].ToString();
            strSQL = "SELECT * FROM [" + firstSheetName + "]";
            da = new OleDbDataAdapter(strSQL, excelConnection);
            da.Fill(dt);

            da.Dispose();
            excelConnection.Close();
            excelConnection.Dispose();
Gratzy
+1 for the suggestion to use ADO.NET. I would update your answer with a link on how to do this OleDb (there are lots out there).
RichardOD
Thanks, I added a simple sample
Gratzy
Thanks, I'll give this a shot. Anything to avoid interop makes me happy. I assume the providers are installed with office? I have to make sure these are present on our prod box. Regarding the cleanup, I tried adding the garbage collection, but did not add the FinalRelease calls. I will try those first to see if they get the job done.
IronicMuffin
You are better off with ado.net If all you need to do is retrieve data and not update I would go that route. Also the connection string is for excel 2007
Gratzy
Yeah I see that. I have the 2000 connection string somewhere from a different project. Thanks again.
IronicMuffin
+1  A: 

Creating/destroying excel on every request will have absolutely terrible performance to matter what you do. In general running any Office app using automation is a nasty business for a lot of reasons (see here). The only way I got it to work is to have a single instance of the app (in my case Word) which is initialized once, and then requests are queued to this instance for processing

If you can stay away from the apps and parse the file yourself (using MS libraries, of just XML)

mfeingold
The requests are few and far between, the app is in house and the feature is an extra... I don't mind the performance hit as long as it works. Thank you for the article, it was definitely eye-opening...I may just ditch the interop and go with the ADO solution above.
IronicMuffin
+1 indeed, keeping away from the Interop and automation is a good move. Later versions of windows using Office 2007 simply do not work without hacks to the OS which isn't realistic for a commercial app.Checkout my post http://stackoverflow.com/questions/1273116/reading-excel-files-as-a-server-process for a summary of 3rd party libraries to do the job for you.
Ian
A: 

The error is probably exactly what it says, you are getting an out of memory error. Try to split the loading of the values array into several smaller chunks instead of getting the entire Range at one time. I tried your code out in C# and had no issues, but my spreadsheet I was loading was mostly empty.

I noticed that the Range was the entire spreadsheet though (from A2 to IV65536 or something). I'm not sure if that is intended.

One thing you could try using is sheet.UsedRange, that will cut down on the number of cells you are loading.

A couple additional small things that I have learned which you may find useful:

  • Use Application instead of ApplicationClass
  • Use Marshal.FinalReleaseComObject(range) (and also for sheet, book, app) otherwise you will have your EXCEL.EXE process sticking around.
David Dietrich
Fair enough on the OOM error...I just find it hard to believe since there are really only 24 cells being imported... it is definitely not my intention to bring in all 65536 rows and columns...that looks like my bug right there. Thanks.
IronicMuffin
+1  A: 

You're going to run into a lot of trouble using interop from ASP.NET. Unless this is meant for some tiny in house application it would be advisable not to go forward with it.

Office Interop is not a programming API in the traditional sense - it's the Office macro system taken to its maximum, with the ability to work interprocess - for example an Excel macro could interact with Outlook.

Some consequences of using interop are:

  • You are actually opening a full copy of the office application.
  • Your actions are being executed by the application as if a user initiated them - that means instead of error messages being returned in code, they are displayed in the GUI.
  • Your copy of the application only closes if you explicitly command it - and even then errors can prevent that from actually happening (the application may present a "do you want to save" dialog if you did not programmably tell Excel that the changes do not need to be saved). This usually results in many hidden copies of Excel being left running on the system - open task manager and see how many excel.exe processes are running.

All of this makes interop something to avoid for regular desktop application, and something that should only be used as a last resort for server applications, since a GUI popup requiring action or script that leaks process is murder on a server environment.

Some alternatives include:

  • Using Microsoft Office 2007 XML based formats, so that you can write the XML files yourself.
  • Using SpreadsheetGear.Net, which is a .NET binary Excel file reader/writer (you don't need Excel installed, as it is completely stand alone). SpreadsheetGear models itself after the Intertop interfaces to make conversion of older code easier.
David
I am aware that it opens the app and processes it as mentioned. This is a tiny in-house app, so efficiency isn't really the goal. I just need to add the feature so they can use it if needed. I'd love to use '07, but we are stuck on '00 here. I will take a look at the link you provided. Thanks.
IronicMuffin
A: 

Maybe you can try some third party excel components,for example SmartXLS for .net,it can read/write excel files without Excel installed.

liya