I have this piece of code for reading from an excel cell:
public T GetValue<T>(string testsheet, string range)
{
Application excelApplication = null;
Workbooks workBooks = null;
Workbook activeWorkBook = null;
Worksheet activeWorkSheet = null;
try
{
excelApplication = new Application();
workBooks = excelApplication.Workbooks;
activeWorkBook = workBooks.Open(workBook);
activeWorkSheet = activeWorkBook.ActiveSheet;
var cells = activeWorkSheet.get_Range(range);
return cells.Value2;
}
catch (Exception theError)
{
Console.WriteLine(theError.Message);
throw theError;
}
finally
{
ReleaseComObject(activeWorkSheet);
ReleaseComObject(activeWorkBook);
ReleaseComObject(workBooks);
ReleaseComObject(excelApplication);
}
}
Also I have a Set value method which sets the value to the cell as below:
public void SetValue<T>(string testsheet, string range, T value)
{
Application excelApplication = null;
Workbooks workBooks = null;
Workbook activeWorkBook = null;
Worksheet activeWorkSheet = null;
try
{
excelApplication = new Application();
workBooks = excelApplication.Workbooks;
activeWorkBook = workBooks.Open(workBook);
activeWorkSheet = activeWorkBook.ActiveSheet;
var cells = activeWorkSheet.get_Range(range);
cells.Value2 = value;
activeWorkBook.Save();
}
catch (Exception theError)
{
Console.WriteLine(theError.Message);
throw theError;
}
finally
{
if (activeWorkBook != null)
activeWorkBook.Close();
ReleaseComObject(excelApplication);
ReleaseComObject(workBooks);
ReleaseComObject(activeWorkBook);
ReleaseComObject(activeWorkSheet);
}
}
Here is my ReleaseComObject:
private static void ReleaseComObject<T>(T comObject) where T : class
{
if (comObject != null)
Marshal.ReleaseComObject(comObject);
}
I have written a test to ensure that the excel objects are properly disposed as below:
[Test]
public void Should_dispose_excel_objects_created_after_io_operation()
{
var expected = Process.GetProcesses().Count(process => process.ProcessName.ToLower() == "excel");
var automationClient = new ExcelAutomation.ExcelAutomationClient(ExcelSheet);
automationClient.SetValue("Sheet1", "A1", 1200);
automationClient.GetValue<double>("Sheet1", "A1");
var actual = Process.GetProcesses().Count(process => process.ProcessName.ToLower() == "excel");
actual.Should().Be(expected, "Excel workbook is not disposed properly. There are still excel processess in memory");
}
This test passess if I'm invoking only SetValue method, however while invoking GetValue it fails. However I can see no Excel.exe in the taskmanager. Any idea why this is happening please? Is something wrong with my GetValue function?
When you done disposing the object, use
GC.Collect();
This is how I dispose my Excel object
private void releaseObject(object obj)
{
try
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
obj = null;
}
catch (Exception ex)
{
obj = null;
MessageBox.Show("Exception Occured while releasing object " + ex.ToString());
}
finally
{
GC.Collect();
}
}
Related
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);
}
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();
}
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;
}
If I have a reference to Worksheet and I close it's parent Workbook, the reference doesn't go away. But I can't figure out how I should check to make sure these sheets don't exist. Checking for null doesn't work.
Example:
Workbook book = Globals.ThisAddIn.Application.ActiveWorkbook;
Worksheet sheet = (Worksheet)book.Worksheets[1]; // Get first worksheet
book.Close(); // Close the workbook
bool isNull = sheet == null; // false, worksheet is not null
string name = sheet.Name; // throws a COM Exception
This is the exception I get when I try to access the sheet:
System.Runtime.InteropServices.COMException was caught
HResult=-2147221080
Message=Exception from HRESULT: 0x800401A8
Source=MyProject
ErrorCode=-2147221080
StackTrace:
at Microsoft.Office.Interop.Excel._Worksheet.get_Name()
at MyCode.test_Click(Object sender, RibbonControlEventArgs e) in c:\MyCode.cs:line 413
InnerException:
This wouldn't even be an issue if I could check for a workbook delete event, but Excel doesn't provide one (which is really annoying).
Is there some convenient way to make sure I don't use these worksheets?
If the other solutions fail, another way to handle this is to store the name of the workbook after it opens, then check to see if that name exists in the Workbooks collection before referencing the sheet. Referencing the workbooks by name will work since you can only have uniquely named workbooks in each instance of Excel.
public void Test()
{
Workbook book = Globals.ThisAddIn.Application.ActiveWorkbook;
string wbkName = book.Name; //get and store the workbook name somewhere
Worksheet sheet = (Worksheet)book.Worksheets[1]; // Get first worksheet
book.Close(); // Close the workbook
bool isNull = sheet == null; // false, worksheet is not null
string name;
if (WorkbookExists(wbkName))
{
name = sheet.Name; // will NOT throw a COM Exception
}
}
private bool WorkbookExists(string name)
{
foreach (Microsoft.Office.Interop.Excel.Workbook wbk in Globals.ThisAddIn.Application.Workbooks)
{
if (wbk.Name == name)
{
return true;
}
}
return false;
}
Edit: for completeness, a helper extension method:
public static bool SheetExists(this Excel.Workbook wbk, string sheetName)
{
for (int i = 1; i <= wbk.Worksheets.Count; i++)
{
if (((Excel.Worksheet)wbk.Worksheets[i]).Name == sheetName)
{
return true;
}
}
return false;
}
I use this method :
private void releaseObject(object obj)
{
try
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
obj = null;
}
catch (Exception ex)
{
obj = null;
MessageBox.Show("Exception Occured while releasing object " + ex.ToString());
}
finally
{
GC.Collect();
}
}
or you can try something like this:
static bool IsOpened(string wbook)
{
bool isOpened = true;
Excel.Application exApp;
exApp = (Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
try
{
exApp.Workbooks.get_Item(wbook);
}
catch (Exception)
{
isOpened = false;
}
return isOpened;
}
I've not tried this, but you could check if the Workbook sheet.Parent exists in the Application.Workbooks collection.
When I run following code it still doesn't releasing excel object.
public static List<string> GetExcelSheets(string FilePath)
{
Microsoft.Office.Interop.Excel.Application ExcelObj = null;
Workbook theWorkbook = null;
Sheets sheets = null;
try
{
ExcelObj = new Microsoft.Office.Interop.Excel.Application();
if (ExcelObj == null)
{
MessageBox.Show("ERROR: EXCEL couldn't be started!");
System.Windows.Forms.Application.Exit();
}
theWorkbook = ExcelObj.Workbooks.Open(FilePath, 0, true, 5, "", "", true, XlPlatform.xlWindows, "\t", false, false, 0, true);
List<string> excelSheets = new List<string>();
sheets = theWorkbook.Worksheets;
foreach (Worksheet item in sheets)
{
excelSheets.Add(item.Name);
}
return excelSheets;
}
catch (Exception ex)
{
return new List<string>();
}
finally
{
// Clean up.
releaseObject(sheets);
releaseObject(theWorkbook);
releaseObject(ExcelObj);
}
}
private static void releaseObject(object obj)
{
try
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
obj = null;
}
catch (Exception ex)
{
obj = null;
MessageBox.Show("Exception Occured while releasing object " + ex.ToString());
}
finally
{
GC.Collect();
}
}
You forgot to keep a reference to ExcelObj.Workbooks and to release it:
Workbooks workbooks;
...
workbooks = ExcelObj.Workbooks;
theWorkbook = workbooks.Open(...
...
releaseObject(sheets);
releaseObject(theWorkbook);
releaseObject(workbooks);
releaseObject(ExcelObj);
Try this code:
ExcelObj.Quit();