Excel process not closing [duplicate] - c#

This question already has answers here:
How do I properly clean up Excel interop objects?
(43 answers)
Closed 9 years ago.
I've got this C# program that never closes the Excel process. Basically it finds the number of instances a string appears in a range in Excel. I've tried all kinds of things, but it's not working. There is a Form that is calling this method, but that shouldn't change why the process isn't closing. I've looks at suggestions by Hans Passant, but none are working.
EDIT: I tried the things mentioned and it still won't close. Here's my updated code.
EDIT: Tried the whole Process.Kill() and it works, but it seems like a bit of a hack for something that should just work.
public class CompareHelper
{
// Define Variables
Excel.Application excelApp = null;
Excel.Workbooks wkbks = null;
Excel.Workbook wkbk = null;
Excel.Worksheet wksht = null;
Dictionary<String, int> map = new Dictionary<String, int>();
// Compare columns
public void GetCounts(string startrow, string endrow, string columnsin, System.Windows.Forms.TextBox results, string excelFile)
{
results.Text = "";
try
{
// Create an instance of Microsoft Excel and make it invisible
excelApp = new Excel.Application();
excelApp.Visible = false;
// open a Workbook and get the active Worksheet
wkbks = excelApp.Workbooks;
wkbk = wkbks.Open(excelFile, Type.Missing, true);
wksht = wkbk.ActiveSheet;
...
}
catch
{
throw;
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
if (wksht != null)
{
//wksht.Delete();
Marshal.FinalReleaseComObject(wksht);
wksht = null;
}
if (wkbks != null)
{
//wkbks.Close();
Marshal.FinalReleaseComObject(wkbks);
wkbks = null;
}
if (wkbk != null)
{
excelApp.DisplayAlerts = false;
wkbk.Close(false, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(wkbk);
wkbk = null;
}
if (excelApp != null)
{
excelApp.Quit();
Marshal.FinalReleaseComObject(excelApp);
excelApp = null;
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
/*
Process[] processes = Process.GetProcessesByName("EXCEL");
foreach (Process p in processes)
{
p.Kill();
}
*/
}
}
}

Here is an interesting knowledge base on the subject of office apps staying open after a .NET app disconnects from them.
Office application does not quit after automation from Visual Studio .NET client
The code examples are all in the link (vb.net sorry). Basically it shows you how to correctly setup and tear down the office app so that it closes when you're finished with it.
System.Runtime.InteropServices.Marshal.FinalReleaseComObject is where the magic happens.
EDIT: You need to call the FinalReleaseComObject for each excel object that you've created.
if (excelWorkSheet1 != null)
{
Marshal.FinalReleaseComObject(excelWorkSheet1);
excelWorkSheet1 = null;
}
if (excelWorkbook != null)
{
Marshal.FinalReleaseComObject(excelWorkbook);
excelWorkbook = null;
}
if (excelApp != null)
{
Marshal.FinalReleaseComObject(excelApp);
excelApp = null;
}

I finally got it to close. You need to add a variable for the Workbooks collection, and then use the FinalReleaseComObject as stated in the other answers. I guess every possible Excel COM object that you use must be disposed this way.
try
{
// Create an instance of Microsoft Excel and make it invisible
excelApp = new Excel.Application();
excelApp.DisplayAlerts = false;
excelApp.Visible = false;
// open a Workbook and get the active Worksheet
excelWorkbooks = excelApp.Workbooks;
excelWorkbook = excelWorkbooks.Open(excelFile, Type.Missing, true);
excelWorkSheet1 = excelWorkbook.ActiveSheet;
}
catch
{
throw;
}
finally
{
NAR( excelWorkSheet1 );
excelWorkbook.Close(false, System.Reflection.Missing.Value, System.Reflection.Missing.Value);
NAR(excelWorkbook);
NAR(excelWorkbooks);
excelApp.Quit();
NAR(excelApp);
}
}
private void NAR(object o)
{
try
{
System.Runtime.InteropServices.Marshal.FinalReleaseComObject( o );
}
catch { }
finally
{
o = null;
}
}

DotNet only release the COM object after all the handles have been released. What I do is comment everything out, and then add back a portion. See if it release Excel. If it did not follow the following rules. When it release, add more code until it does not release again.
1) When you create your Excel variables, set all the values to null (this avoid not initiated errors)
2) Do not reuse variables without releasing it first Marshal.FinalReleaseComObject
3) Do not double dot (a.b = z). dotNet create a temporary variable, which will not get released.
c = a.b;
c = z;
Marshal.FinalReleaseComObject(c);
4) Release ALL excel variables. The quicker the better.
5) Set it back to NULL.
Set culture to "en-US". There is a bug that crash Excel with some cultures. This ensure it won't.
Here is an idea of how your code should be structured:
thisThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
InteropExcel.Application excelApp = null;
InteropExcel.Workbooks wkbks = null;
InteropExcel.Workbook wkbk = null;
try
{
excelApp = new InteropExcel.Application();
wkbks = excelApp.Workbooks;
wkbk = wkbks.Open(fileName);
...
}
catch (Exception ex)
{
}
if (wkbk != null)
{
excelApp.DisplayAlerts = false;
wkbk.Close(false);
Marshal.FinalReleaseComObject(wkbk);
wkbk = null;
}
if (wkbks != null)
{
wkbks.Close();
Marshal.FinalReleaseComObject(wkbks);
wkbks = null;
}
if (excelApp != null)
{
// Close Excel.
excelApp.Quit();
Marshal.FinalReleaseComObject(excelApp);
excelApp = null;
}
// Change culture back from en-us to the original culture.
thisThread.CurrentCulture = originalCulture;
}

Related

C# Prevent users from losing data in excel interop

I am writing an application in C# that changes data in an excel worksheet. VBA isn't an option because the version of office that is installed on the clients desktop is Microsoft Office 2010 Starter Edition which doesn't support VBA (none of the starter editions do). The application is using the excel interop library.
When I start the application it checks to see if the excel workbook that is to be modified is open and if it is open it notifies the user and then quits. This part is working as expected. The check isn't working if the user opens the excel file for some reason after starting the application and then trying to save their work from inside the application. In that case any modifications from the application are lost without any error notification. If you need to see more of the code to answer the entire project is in GitHub.
I've tried changing CheckExcelWorkBookOpen from a static class to a class that gets instantiated every time it is used, just in case the list of open workbooks was being stored in the excel interop library, this did not help.
The code that works in the application start up is:
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(Preferences.ExcelWorkBookFullFileSpec);
The code is also called any time the application attempts to open the file either for input or output, this doesn't work:
private void StartExcelOpenWorkbook()
{
if (xlApp != null)
{
return;
}
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(WorkbookName);
xlApp = new Excel.Application();
xlApp.Visible = false;
xlApp.DisplayAlerts = false;
xlWorkbook = xlApp.Workbooks.Open(WorkbookName);
}
Current Code
using System;
using Excel = Microsoft.Office.Interop.Excel;
namespace TenantRosterAutomation
{
public class CheckExcelWorkBookOpen
{
// Check if there is any instance of excel open using the workbook.
public static bool IsOpen(string workBook)
{
Excel.Application TestOnly = null;
bool isOpened = true;
// There are 2 possible exceptions here, GetActiveObject will throw
// an exception if no instance of excel is running, and
// workbooks.get_Item throws an exception if the sheetname isn't found.
// Both of these exceptions indicate that the workbook isn't open.
try
{
TestOnly = (Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
int lastSlash = workBook.LastIndexOf('\\');
string fileNameOnly = workBook.Substring(lastSlash + 1);
TestOnly.Workbooks.get_Item(fileNameOnly);
TestOnly = null;
}
catch (Exception)
{
isOpened = false;
if (TestOnly != null)
{
TestOnly = null;
}
}
return isOpened;
}
// Common error message to use when the excel file is op in another app.
public string ReportOpen()
{
string alreadyOpen = "The excel workbook " +
Globals.Preferences.ExcelWorkBookFullFileSpec +
" is alread open in another application. \n" +
"Please save your changes in the other application and close the " +
"workbook and then try this operation again or restart this application.";
return alreadyOpen;
}
public void TestAndThrowIfOpen(string workBook)
{
if (IsOpen(workBook))
{
AlreadyOpenInExcelException alreadOpen =
new AlreadyOpenInExcelException(ReportOpen());
throw alreadOpen;
}
}
}
}
This code is now included in a question on code review.
I got the above code to work by ensuring that any excel process started by the application was killed after the task was complete. The following code is added to my ExcelInterface module. The Dispose(bool) function already existed but did not kill the process:
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (xlWorkbook != null)
{
xlWorkbook.Close();
xlWorkbook = null;
}
if (xlApp != null)
{
xlApp.Quit();
xlApp = null;
Process xlProcess = Process.GetProcessById(ExcelProcessId);
if (xlProcess != null)
{
xlProcess.Kill();
}
}
}
disposed = true;
}
}
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
private int GetExcelProcessID(Excel.Application excelApp)
{
int processId;
GetWindowThreadProcessId(excelApp.Hwnd, out processId);
return processId;
}
private void StartExcelOpenWorkbook()
{
if (xlApp != null)
{
return;
}
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(WorkbookName);
xlApp = new Excel.Application();
xlApp.Visible = false;
xlApp.DisplayAlerts = false;
xlWorkbook = xlApp.Workbooks.Open(WorkbookName);
ExcelProcessId = GetExcelProcessID(xlApp);
}

Disposing of COM object from c#

I have read several articles/posts on correctly disposing of COM objects when interacting from .NET. Normally i don't have a problem.. but i keep getting orphaned COM objects after closing my following program.I don't know if it has something to do with the addin i am opening.
Application app = new Application();
Workbooks wrks = app.Workbooks;
Workbook wrk = null;
Workbook wrkAddin = null;
Sheets shts = null;
Range rng = null;
try
{
app.ScreenUpdating = true;
app.Visible = true;
app.DisplayAlerts = false; // stop any dialog boxes appearing.. such as save or debug when loading bloomberg addin.
wrk = wrks.Open(Filename: MasterPriceFiles.Properties.Resources.strETFDataFullPath, ReadOnly: false,Editable: true, IgnoreReadOnlyRecommended: true);
Thread.Sleep(500);
wrkAddin = wrks.Open(Filename: MasterPriceFiles.Properties.Resources.strBloombergAddinPath); // have to explicitly open the bloomberg addin
app.Visible = true;
Thread.Sleep(50000);
shts = wrk.Worksheets;
foreach (Worksheet sht in shts)
{
rng = sht.UsedRange;
rng.Copy();
rng.PasteSpecial(XlPasteType.xlPasteValues);
Marshal.ReleaseComObject(rng);
Marshal.ReleaseComObject(sht);
}
app.Visible = false;
app.DisplayAlerts = false;
Thread.Sleep(500);
wrk.SaveAs(Filename: MasterPriceFiles.Properties.Resources.strETFDataMacroFreeFullPath, ConflictResolution: XlSaveConflictResolution.xlLocalSessionChanges, AccessMode: XlSaveAsAccessMode.xlNoChange);
Console.WriteLine("Successfully saved ETF price data sheet at . " + DateTime.Now.ToString());
}
catch(Exception ex)
{
Console.WriteLine("Error refreshing ETF price date. " + ex.Message);
}
finally
{
if (rng != null) Marshal.FinalReleaseComObject(rng);
if (shts != null) Marshal.FinalReleaseComObject(shts);
wrk.Close(SaveChanges: false);
if (wrk != null) Marshal.FinalReleaseComObject(wrk);
wrkAddin.Close(SaveChanges: false);
if (wrkAddin != null) Marshal.FinalReleaseComObject(wrkAddin);
if (wrks != null) Marshal.FinalReleaseComObject(wrks);
app.Quit();
if (app != null) Marshal.FinalReleaseComObject(app);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}

Excel Process will not terminate with Excel-Introp

I have been trying to import data from an excel sheet into a DataTable. The problem is that after i finish, excel would not terminate the process. I do not want to terminate by process name using System.Diagnostics as that approach would terminate all excel instances rather than the one that was created by the application. I know that this question was posted over here before multiple times, but none of the solutions seem to work for me. I am probably missing something and can not see it.
below is my code:
private void importToolStripMenuItem_Click(object sender, EventArgs e)
{
openFileDialog1.Filter = "Excel WorkBooks (*.xlsx)|*.xlsx";
openFileDialog1.FilterIndex = 1;
openFileDialog1.Multiselect = false;
if(openFileDialog1.ShowDialog() == DialogResult.OK)
{
string empRange = "D4";
string emptxid = "K4";
object oMissing = System.Reflection.Missing.Value;
string path = openFileDialog1.FileName;
oExcel.Application xlApp;
oExcel.Workbooks xlWorkBooks;
oExcel.Workbook xlWorkBook;
oExcel.Sheets xlSheets;
oExcel.Worksheet xlWorkSheetDATA;
oExcel.Worksheet xlWorkSheetEMP;
oExcel.Range range;
xlApp = new oExcel.Application();
xlWorkBooks = xlApp.Workbooks;
xlWorkBook = xlWorkBooks.Open(path);
xlSheets = xlWorkBook.Worksheets;
xlWorkSheetDATA = (oExcel.Worksheet)xlSheets.get_Item(DATASheetName);
xlWorkSheetEMP = (oExcel.Worksheet)xlSheets.get_Item(EMPSheetName);
xlWorkSheetEMP.Activate();
range = xlWorkSheetEMP.get_Range(empRange, empRange);
xlWorkSheetDATA.Activate();
range = xlWorkSheetDATA.get_Range(emptxid, emptxid);
xlApp.DisplayAlerts = false;
xlWorkSheetDATA.Columns.ClearFormats();
xlWorkSheetDATA.Rows.ClearFormats();
int iTotalColumns = xlWorkSheetDATA.UsedRange.Columns.Count;
int iTotalRows = xlWorkSheetDATA.UsedRange.Rows.Count;
xlApp.Visible = true;
DataTable dt = new DataTable();
addColumns(iTotalColumns, dt);
insertIntoDataTable(iTotalRows, dt, path);
//clean-up
System.Runtime.InteropServices.Marshal.ReleaseComObject(range);
range = null;
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkSheetEMP);
xlWorkSheetEMP = null;
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkSheetDATA);
xlWorkSheetDATA = null;
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheets);
xlSheets = null;
xlWorkBook.Close(false);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkBook);
xlWorkBook = null;
xlWorkBooks.Close();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkBooks);
xlWorkBooks = null;
xlApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
xlApp = null;
}
}
Write a method to release the objects such as this example
private void ReleaseOfficeObject(object o)
{
Marshal.ReleaseComObject(o);
GC.Collect();
GC.WaitForPendingFinalizers();
Marshal.FinalReleaseComObject(o);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
then to call it
ReleaseOfficeObject(range);
ReleaseOfficeObject(xlWorkSheetDATA);
ReleaseOfficeObject(xlSheets);
xlWorkBook.Close(false);
ReleaseOfficeObject(xlWorkBook);
xlWorkBook = null;
xlWorkBooks.Close();
ReleaseOfficeObject(xlWorkBooks);
xlWorkBooks = null;
xlApp.Quit();
ReleaseOfficeObject(xlApp);
You never need to call Marshal.ReleaseComObject in this context. The runtime is perfectly able to keep track of COM objects and release them when they are no longer referenced. Calling Marshal.ReleaseComObject is a confusing anti-pattern that sadly even some Microsoft documentation mistakenly suggests. You also don't have to set local variables to null - the references in local variables will be out of scope when the method completes.
You do have to be careful with this kind of code in debug builds. References in a method are artificially kept alive until the end of the method so that they will still be accessible in the debugger. This means your local xlApp variable might not be cleaned up by calling the GC inside that method.
You should run the garbage collection twice, reference cycles could be broken in the first collection, but you need a second collection to be sure all cleanup is done for those references.
To avoid this issue, you might follow a pattern like this:
private void importToolStripMenuItem_Click(object sender, EventArgs e)
{
openFileDialog1.Filter = "Excel WorkBooks (*.xlsx)|*.xlsx";
openFileDialog1.FilterIndex = 1;
openFileDialog1.Multiselect = false;
if(openFileDialog1.ShowDialog() == DialogResult.OK)
{
string path = openFileDialog1.FileName;
// Call another method that wraps all the Excel COM stuff
// That prevents debug builds from confusing the lifetime of COM references
DoExcelStuff(path);
// When back here, all the Excel references should be out of scope
// Run the GC (twice) to clean up all COM references
// (This can be pulled out into a helper method somewhere)
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
private void DoExcelStuff(string path)
{
// All your Excel COM interop goes here
string empRange = "D4";
string emptxid = "K4";
object oMissing = System.Reflection.Missing.Value;
// ... variable declarations
xlApp = new oExcel.Application();
// More xlApp, workbooks etc...
// Done - tell Excel to close
xlApp.Quit();
// No need for Marshal.ReleaseComObject(...)
// or setting variables to null here!
}

Excel.Interop.Chart.Export does not close exported image file

I have made a small class that takes an excel workbook, graps the contained charts in the workbook and export each of them to PNG files.
It almost works fine, but I have a problem regarding the generated PNG files, the class doesn't close the files properly, until the user closes the application.
Here is my class:
public List<string> ExportCharts(string fileName)
{
var exportedGraphsList = new List<string>();
Excel.Workbooks xlWorkBooks = null;
Excel.Workbook xlWorkBook = null;
Excel.Application xlApp = null;
Excel.Sheets xlSheets = null;
object misValue = System.Reflection.Missing.Value;
try
{
xlApp = new Excel.ApplicationClass();
xlWorkBooks = xlApp.Workbooks;
xlWorkBook = xlWorkBooks.Open(fileName);
xlSheets = xlWorkBook.Charts;
foreach (Excel.Chart xlChart in xlSheets)
{
var exportfileName = ConfigurationManager.AppSettings["imageSavePath"] + #"\" + xlChart.Name + ".png";
xlChart.Export(exportfileName, "PNG", misValue);
exportedGraphsList.Add(exportfileName);
}
return exportedGraphsList;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + "\n\n" + ex.StackTrace, "Der gik noget galt");
exportedGraphsList.Clear();
return exportedGraphsList;
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
if(xlSheets != null)
Marshal.FinalReleaseComObject(xlSheets);
if (xlWorkBook != null)
{
xlWorkBook.Close(false, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlWorkBook);
}
if (xlWorkBooks != null)
Marshal.FinalReleaseComObject(xlWorkBooks);
if (xlApp != null)
{
xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);
}
}
}
public void Dispose()
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
When this method is invoked, it starts and closes the Excel proces very nicely, but the PNG files remains open. I could of coruse put a timestamp on the exported filesnames, but I fear it will confuse the user.
Thanks in advance for any hints or ideas. :-)
Solved - it was due to the fact that I loaded the image files afterwards to the GUI.
So Excel.InterOp works fine.

Excel process remains open after interop; traditional method not working

I'm running into an issue with some code I'm debugging. Excel interop is used to extract some values from a workbook; however, Excel remains open after the program has exited. I've tried the traditional solution, but it still keeps a reference to Excel open on all machines where the code is run
private void TestExcel()
{
Excel.Application excel = new Excel.Application();
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open("C:\\test.xlsm");
book.Close();
books.Close();
excel.Quit();
Marshal.ReleaseComObject(book);
Marshal.ReleaseComObject(books);
Marshal.ReleaseComObject(excel);
}
Even this simple piece of code keeps the process running with multiple files (xlsm, xlsx, xls). Right now we have a workaround in place to kill the Excel processes we've opened, but I'd much rather get this working for my own sanity.
I should add that I have it narrowed down to the Workbook variable. If I remove the call to books.Open() and all references to book then it closes successfully.
This has worked successfully for me:
xlApp.Quit();
//release all memory - stop EXCEL.exe from hanging around.
if (xlWorkBook != null) { Marshal.ReleaseComObject(xlWorkBook); } //release each workbook like this
if (xlWorkSheet != null) { Marshal.ReleaseComObject(xlWorkSheet); } //release each worksheet like this
if (xlApp != null) { Marshal.ReleaseComObject(xlApp); } //release the Excel application
xlWorkBook = null; //set each memory reference to null.
xlWorkSheet = null;
xlApp = null;
GC.Collect();
This code works for me.
//Declare separate object variables
Excel.Application xlApp = new Excel.Application();
Excel.Workbooks xlWorkbooks = xlApp.Workbooks;
Excel.Workbook xlWorkbook = xlWorkbooks.Add(Missing.Value);
Excel.Worksheet xlWorksheet = (Excel.Worksheet)xlWorkbook.Worksheets.get_Item(1);
//Create worksheet
xlWorkbook.Close(false, Missing.Value, Missing.Value);
xlWorkbooks.Close();
xlApp.Quit();
Marshal.FinalReleaseComObject(xlWorksheet);
Marshal.FinalReleaseComObject(xlWorkbook);
Marshal.FinalReleaseComObject(xlWorkbooks);
Marshal.FinalReleaseComObject(xlApp);
xlWorksheet = null;
xlWorkbook = null;
xlWorkbooks = null;
xlApp = null;
GC.Collect();
This article from Microsoft has some good information regarding this issue.
I am a total COM amateur, used it for a minor thing in one project quite a long time ago, but here's a snippet I used there. I probably found it somewhere online, don't remember. In any case, I paste it its full glory ;)
public static class ComBlackBox
{
public static void ReleaseObject(object obj)
{
try
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
obj = null;
}
catch (ArgumentException ex)
{
obj = null;
MessageBox.Show("Unable to release the Object " + ex.Message);
}
finally
{
GC.Collect();
}
}
}
I'm unable to try it out now, but it probably worked (I honestly don't remember any details). Maybe it will help you out. Feel free to point out any obvious problems with this code, I really am far from being COM-literate ;)
This is how I got around this problem:
// Store the Excel processes before opening.
Process[] processesBefore = Process.GetProcessesByName("excel");
// Open the file in Excel.
Application excelApplication = new Application();
Workbook excelWorkbook = excelApplication.Workbooks.Open(Filename);
// Get Excel processes after opening the file.
Process[] processesAfter = Process.GetProcessesByName("excel");
// Now find the process id that was created, and store it.
int processID = 0;
foreach (Process process in processesAfter)
{
if (!processesBefore.Select(p => p.Id).Contains(process.Id))
{
processID = process.Id;
}
}
// Do the Excel stuff
// Now close the file with the COM object.
excelWorkbook.Close();
excelApplication.Workbooks.Close();
excelApplication.Quit();
// And now kill the process.
if (processID != 0)
{
Process process = Process.GetProcessById(processID);
process.Kill();
}

Categories