简体   繁体   中英

Cleaning up Excel Interop

So I have a Winforms application that needs to read from an Excel sheet to populate some fields. To make the UI responsive, I decided to create a thread to read from the Excel sheet so the main thread does not have to wait. If the read finishes, and then the application exits, EXCEL.EXE behaves nicely and exits. But if I close the main application while the read is still ongoing, then EXCEL.EXE task stays alive.

I'm guessing that is is because ExcelReader does not have the time to call the destructor before it closes?

One possible solution would be to have the main form call ExcelReader.Cleanup in it's FormClosing event. But that seems to be a horrible violation of encapsulation.

What other possible solutions are there to this? Here's my code for the ExcelReader:

using Excel = Microsoft.Office.Interop.Excel;

class ExcelReader
{
    private int sheetNum { get ; set; }
    public int rowCount { get; private set; }
    public int colCount { get; private set; }
    public List<string> sheetValues { get; private set; }
    public List<string> sheetNames { get; private set; }

    Excel.Application xlApp;

    Excel.Workbooks workBooks;
    Excel.Workbook xlWorkbook;

    Excel.Worksheet xlWorkSheet;
    Excel.Range xlRange;

    Excel.Range row;
    Excel.Range col;


    public ExcelReader(string path){
        //initialize values
        this.sheetNum = 1;
        sheetNames = new List<string>();
        sheetValues = new List<string>();

        //read from excel blackmagic here
        xlApp = new Excel.Application();

        workBooks = xlApp.Workbooks;
        xlWorkbook = workBooks.Open(path);

        xlWorkSheet = xlWorkbook.Sheets[sheetNum];
        xlRange = xlWorkSheet.UsedRange;

        row = xlRange.Rows;
        col = xlRange.Columns;

        int rowCount = row.Count;
        int colCount = col.Count;            

        this.getSheetNames(xlWorkbook);
        this.getValues(xlRange, rowCount, colCount);

        CleanUp();
    }

    ~ExcelReader()
    {
        CleanUp();
    }

    private void getSheetNames(Excel.Workbook xlWorkbook)
    {
        var workSheets = xlWorkbook.Sheets;
        int numberOfSheets = workSheets.Count;
        for (int i = 1; i < numberOfSheets+1; i++)
        {
            sheetNames.Add(xlWorkbook.Sheets[i].Name);
        }
        Marshal.FinalReleaseComObject(workSheets);
    }

    private void getValues(Excel.Range xlRange, int rowCount, int colCount)
    {
        for (int i = 1; i < rowCount; i++)
        {
            for (int j = 1; j < colCount; j++)
            {
                var cells = xlRange.Cells[i, j];
                var value = cells.Value2;
                sheetValues.Add(value);
                Marshal.FinalReleaseComObject(cells);
            }
        }
    }

    private void CleanUp()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();

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

        col.Clear();
        row.Clear();
        Marshal.FinalReleaseComObject(col);
        Marshal.FinalReleaseComObject(row);

        xlRange.Clear();
        Marshal.FinalReleaseComObject(xlRange);

        //close book without saving
        xlWorkbook.Close(false);
        workBooks.Close();
        Marshal.FinalReleaseComObject(xlWorkbook);
        Marshal.FinalReleaseComObject(workBooks);

        xlApp.Quit();
        Marshal.FinalReleaseComObject(xlApp);
    }
}

Excel can be a pain to close under certain circumstances during an Interop session. Sometimes you may need to use Windows API calls to force the instance of Excel you're using to close.

Here is a class I've used on several projects to manage Excel in Interop. The key points here are to use a unique caption, in this case a GUID, to identify the specific instance of Excel you're working with and to use this information to force the instance to close if the Quit method doesn't do the trick.

using System;
using System.Runtime.InteropServices;  
namespace ExcelSupport
{
    public class ExcelController : IDisposable
    {
        private Microsoft.Office.Interop.Excel.Application _ExcelApplication;
        private bool disposedValue = false;   // To detect redundant calls
        private string _caption; //used to uniquely identify hidden Excel instance
        //
        // Windows API used to help close Excel instance
        //
        [DllImport("user32.dll")] 
        private static extern int EndTask(IntPtr hWnd);
        [DllImport("user32.dll")]
        private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
        [DllImport("user32.dll")]
        private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
        [DllImport("kernel32.dll")] 
        private static extern IntPtr SetLastError(int dwErrCode);

    public ExcelController()
    {
        OpenExcel(null);
    }

    public ExcelController(string workbookName)
    {
        OpenExcel(workbookName);
    }

    private void OpenExcel(string workbookName)
    {
        _ExcelApplication = new Microsoft.Office.Interop.Excel.Application();
        _caption = System.Guid.NewGuid().ToString().ToUpper();
        _ExcelApplication.Caption = _caption;
        _ExcelApplication.Visible = false;
        _ExcelApplication.DisplayAlerts = false;
        if(workbookName != null)
            _ExcelApplication.Workbooks.Open(workbookName);
    }

    public Microsoft.Office.Interop.Excel.Application Application
    {
        get { return _ExcelApplication; }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        if (!this.disposedValue)
        {
            try
            {
                if (_ExcelApplication != null)
                {
                    foreach (Microsoft.Office.Interop.Excel.Workbook WorkBookName in _ExcelApplication.Workbooks)
                    {
                        WorkBookName.Close(false);
                    }
                    IntPtr iHandle = IntPtr.Zero;
                    iHandle = new IntPtr(_ExcelApplication.Parent.Hwnd);
                    _ExcelApplication.DisplayAlerts = false;
                    _ExcelApplication.Quit();
                    System.Runtime.InteropServices.Marshal.ReleaseComObject(_ExcelApplication);
                    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(_ExcelApplication);
                    SetLastError(0);
                    if (IntPtr.Equals(iHandle, IntPtr.Zero))
                        iHandle = FindWindow(null, _caption);
                    if (!IntPtr.Equals(iHandle, IntPtr.Zero))
                    {
                        int iRes;
                        uint iProcId;
                        iRes = (int)GetWindowThreadProcessId(iHandle, out iProcId);
                        if (iProcId == 0)
                        {
                            if (EndTask(iHandle) == 0)
                                throw new ApplicationException("Excel Instance Could Not Be Closed");
                        }
                        else
                        {
                            System.Diagnostics.Process proc = System.Diagnostics.Process.GetProcessById((int)iProcId);
                            proc.CloseMainWindow();
                            proc.Refresh();
                            if (!proc.HasExited)
                                proc.Kill();

                        }
                    }
                }
            }
            finally
            {
                _ExcelApplication = null;
                GC.WaitForPendingFinalizers();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }
        }
        this.disposedValue = true;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM