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);
}
Related
I am trying to close excel process in my winform application. I have gone through lots of posts on SO and other sites and this is what I am doing right now:
private void lsvSelectedQ_SelectedIndexChanged(object sender, EventArgs e)
{
FillSelectedItems();
}
private void FillSelectedItems()
{
string filepath = string.Empty;
string reportname = lblreportname.Text;
filepath = Application.StartupPath + "\\StandardReports\\" + reportname + ".xls";
Microsoft.Office.Interop.Excel.Application xlApp;
Microsoft.Office.Interop.Excel.Workbook xlWorkBook;
Microsoft.Office.Interop.Excel.Worksheet xlWorkSheet;
object misValue = System.Reflection.Missing.Value;
xlApp = new Microsoft.Office.Interop.Excel.Application();
xlWorkBook = xlApp.Workbooks.Open(filepath, 0, true, 5, "", "", true, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "\t", false, false, 0, true, 1, 0);
List<string> WorksheetList = new List<string>();
xlWorkSheet = (Microsoft.Office.Interop.Excel.Worksheet)xlWorkBook.Worksheets.get_Item("Config");
Microsoft.Office.Interop.Excel.Range objRange = null;
objRange = (Microsoft.Office.Interop.Excel.Range)xlWorkSheet.Cells[6, 2];
if (objRange.Value != null)
{
int intSheet = Convert.ToInt32(objRange.Value);
for (int i = 0; i < intSheet; i++)
{
objRange = (Microsoft.Office.Interop.Excel.Range)xlWorkSheet.Cells[6, 3+i ];
if (objRange.Value != null)
{
lsvSelectedQ.Items.Add(Convert.ToString(objRange.Value));
}
}
}
ReleaseMyExcelsObjects(xlApp, xlWorkBook, xlWorkSheet);
}
In the above code I am using ReleaseMyExcelsObjects method to get rid of running excel process in the taskbar.
private void ReleaseMyExcelsObjects(Microsoft.Office.Interop.Excel.Application xlApp, Microsoft.Office.Interop.Excel.Workbook xlWorkBook, Microsoft.Office.Interop.Excel.Worksheet xlWorkSheet)
{
xlApp.DisplayAlerts = false;
xlWorkBook.Save();
xlWorkBook.Close();
xlApp.DisplayAlerts = false;
xlApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkSheet);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkBook);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
xlWorkSheet = null;
xlWorkBook = null;
xlApp = null;
GC.Collect();
}
As you can see I open an excel on a SelectedIndexChanged and I am also trying to close the process through ReleaseMyExcelsObjects() method and it works except for the first excel process that is generated. I mean when the event is fired the excel process is started and ReleaseMyExcelsObjects() does not close it however the second time SelectedIndexChanged is fired, another excel process is started. This time ReleaseMyExcelsObjects() closes this second excel process. But the first excel process which was started when theSelectedIndexChanged event was fired for the first time never gets closed.
EDIT:
I have posted an answer myself which is getting the job done. But I am going to keep this question open if in case someone comes up with better solution.
I have found a work around for this:
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
public static Process GetExcelProcess(Microsoft.Office.Interop.Excel.Application excelApp)
{
int id;
GetWindowThreadProcessId(excelApp.Hwnd, out id);
return Process.GetProcessById(id);
}
Use it as classobj.GetExcelProcess(xlApp).Kill();
Taken from : https://stackoverflow.com/a/15556843/2064292
from my understanding, the ReleaseComObject closes the Excel Application when your C# application exits. Thus, a new Excel processes will show up in your taskbar every time you call FillSelectedItems() which won't close until you exit your C# application.
EDIT: On a side note, I recommend using try, catch, finally when handling the excel interop library, mainly due to the fact that if the application runs into an exception, the program will not exit normally, and as stated before it will result on the excel process remaining in the taskbar (and when you manually open said file on excel it will tell you the last recovered version blablabla)
string filepath = string.Empty;
string reportname = lblreportname.Text;
filepath = Application.StartupPath + "\\StandardReports\\" + reportname + ".xls";
Microsoft.Office.Interop.Excel.Application xlApp;
Microsoft.Office.Interop.Excel.Workbook xlWorkBook;
Microsoft.Office.Interop.Excel.Worksheet xlWorkSheet;
object misValue = System.Reflection.Missing.Value;
xlApp = new Microsoft.Office.Interop.Excel.Application();
xlWorkBook = xlApp.Workbooks.Open(filepath, 0, true, 5, "", "", true, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "\t", false, false, 0, true, 1, 0);
List<string> WorksheetList = new List<string>();
try
{
//Your code
}
catch (Exception ex) { MessageBox.Show(string.Format("Error: {0}", ex.Message)); }
finally
{
xlWorkBook.Close(false, filepath, null);
Marshal.ReleaseComObject(xlWorkBook);
}
Honestly, if you are annoyed by this, I would recommend using EPPlus or ExcelDataReader(this one only for reading the excel spreadsheets) as alternative libraries. Otherwise, No matter how many releaseComObjects or garbagecollectors you add, I believe you wont get rid of this issue.
EDIT 2: Of course, another way to go around this is to search for an Excel process ID and kill it. The reason I did not recommend this, is because in the event that the user who's running this application already has another excel process running on his computer, you could end up killing that instance.
I have a solution for closing the Excel process. Instead of going thru the pains of releasing your objects, you can kill that specific excel process (if you have multiple of them open).
The code is:
using System;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;
namespace YourNameSpace
{
public class MicrosoftApplications
{
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
public class Excel
{
public Excel()
{
Application = new Microsoft.Office.Interop.Excel.Application();
RegisterExitEvent();
}
public Microsoft.Office.Interop.Excel.Application Application;
private void RegisterExitEvent()
{
Application.WindowDeactivate -= XlApp_WindowDeactivate;
Application.WindowDeactivate += XlApp_WindowDeactivate;
}
private void XlApp_WindowDeactivate(Workbook Wb, Window Wn)
{
Kill();
}
public void Kill()
{
int pid = 0;
GetWindowThreadProcessId(Application.Hwnd, out pid);
if (pid > 0)
{
System.Diagnostics.Process p = System.Diagnostics.Process.GetProcessById(pid);
p.Kill();
}
Application = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
}
And you can call it by: YourNameSpace.MicrosoftApplications.Excel xlApp = new YourNameSpace.MicrosoftApplications.Excel();
Do whatever you need to do by calling xlApp.Application.whatever instead of xlApp.whatever and if the user exits the excel window(s) it will kill the process(es) that were used in the code. If you want to just generate a report behind the scenes but not display the form, then simply call xlApp.Kill(); to end that specific process.
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;
}
I have a bit of code that opens an xls workbook;
Excel.Workbooks workBooks;
workBooks = excelApp.Workbooks;
workbook = workBooks.Open(sourceFilePath + sourceFileName + ".xls");
I then get the work sheet;
worksheets = workbook.Worksheets;
worksheet = worksheets.get_Item("Standard");
I then save the file as a csv;
worksheet.SaveAs(sourceFilePath + sourceFileName + ".csv", Excel.XlFileFormat.xlCSVWindows, Type.Missing, Type.Missing, false, false, Excel.XlSaveAsAccessMode.xlNoChange, Type.Missing, Type.Missing, Type.Missing);
Then I try to close the workbook;
Marshal.FinalReleaseComObject(worksheet);
Marshal.FinalReleaseComObject(worksheets);
workbook.Close();
Marshal.FinalReleaseComObject(workbook);
However, every time i get to the line workbook.Close(), the system stops.
If I do not do the SaveAs then the workbook closes just fine.
How do I close a workbook?
edit
Looking at Task Manager shows me that Excel.exe is still running. Closing it will produce an error in my code.
edit 2
I have already seen the referenced SO post and it did not solve the issue.
Here is the solution
first:
using EXCEL = Microsoft.Office.Interop.Excel;
and then, path is where your excel locates.
EXCEL.Application excel = new EXCEL.Application();
try
{
EXCEL.Workbook book = excel.Application.Workbooks.Open(path);
EXCEL.Worksheet sheet = book.Worksheets[1];
// yout operation
}
catch (Exception ex) { MessageBox.Show("readExcel:" + ex.Message); }
finally
{
KillExcel(excel);
System.Threading.Thread.Sleep(100);
}
[DllImport("User32.dll")]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int ProcessId);
private static void KillExcel(EXCEL.Application theApp)
{
int id = 0;
IntPtr intptr = new IntPtr(theApp.Hwnd);
System.Diagnostics.Process p = null;
try
{
GetWindowThreadProcessId(intptr, out id);
p = System.Diagnostics.Process.GetProcessById(id);
if (p != null)
{
p.Kill();
p.Dispose();
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("KillExcel:" + ex.Message);
}
}
Why not combine the 2. This will take care of any problems with closing before saving is complete.
There is an option in the Close method to save the file.
workbook.Close(true, fileName, Missing.Value);
Also if the file is saving correctly, and your problem is purely because the excel.exe process is still running, it could be because you didn't close and release EVERYTHING needed. I have had this before and developed a more complete close down routine. My code for shutting down an excel file is:
book.Close(true, fileName, Missing.Value); //close and save individual book
allBooks.Close(); //close all books
excel.Quit();
Marshal.ReleaseComObject(allCells); //any used range objects
Marshal.ReleaseComObject(sheet);
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(book);
Marshal.ReleaseComObject(allBooks);
Marshal.ReleaseComObject(excel);
This works 100% of the time for me.
Have you considered the fact that the system might still be in the process of saving the file when you attempt to close it? I'm just saying, to be sure add a delay(Thread.Sleep(1000) in C# for example) before the close to see if this is the problem.
This question keeps popping up see:
How to properly clean up Excel interop objects in C#
You need to call System.Runtime.InteropServices.Marshal.ReleaseComObject() on every excel object you use, even invisible ones, e.g.:
var worksheet = excelApp.Worksheets.Open()
There are two objects here:
1. The obvious 'Worksheet' opened with Open()
2. The "invisible" collection 'Worksheets'.
Both of them need to be released (so you better keep a reference for Worksheets):
var wkCol = excelApp.Worksheets;
var worksheet = wkCol.Open();
EXCEL.Application excel = new EXCEL.Application();
try
{
EXCEL.Workbook book = excel.Application.Workbooks.Open(path);
EXCEL.Worksheet sheet = book.Worksheets[1];
// yout operation
}
catch (Exception ex) { MessageBox.Show("readExcel:" + ex.Message); }
finally
{
KillExcel(excel);
System.Threading.Thread.Sleep(100);
}
[DllImport("User32.dll")]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int ProcessId);
private static void KillExcel(EXCEL.Application theApp)
{
int id = 0;
IntPtr intptr = new IntPtr(theApp.Hwnd);
System.Diagnostics.Process p = null;
try
{
GetWindowThreadProcessId(intptr, out id);
p = System.Diagnostics.Process.GetProcessById(id);
if (p != null)
{
p.Kill();
p.Dispose();
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("KillExcel:" + ex.Message);
}
}
Thank you!!!!
This work for me. EXCEL.EXE will be kill after close you program.
Excel.Application objExcel;
Excel._Workbook objBook;
Excel.Workbooks objBooks;
Excel._Worksheet objSheet;
Excel.Sheets objSheets;
objExcel = new Excel.Application();
objBooks = objExcel.Workbooks;
objBook = objExcel.Workbooks.Open(Application.StartupPath+#"/"+"template.xls");
objSheets = objBook.Worksheets;
objSheet = (Excel._Worksheet)objBook.ActiveSheet;
//....you code modify excel book
objBook.Close(true, objBook, Missing.Value);
objExcel.Quit();
Marshal.ReleaseComObject(objSheet);
Marshal.ReleaseComObject(objSheets);
Marshal.ReleaseComObject(objBook);
Marshal.ReleaseComObject(objBooks);
Marshal.ReleaseComObject(objExcel);
I have a website where users generate an Excel report using a macro, when I try to run it in my local machine it generates perfectly and runs the macro inside Excel. When I publish it into the server and at the same time I am logged in there (RDP open session) and try to run it from a browser outside that server it is also running as expected. The problem occurs when I am logged off in the server (RDP) then run it in a browser outside the server (ie from my machine) the macro does not run but creates my Excel.
This is the code that I am using
public class Report
{
protected Workbook Workbook { get; set; }
protected Application Excel { get; set; }
public void RunReport()
{
// Launch Excel on the server
Excel = new Application
{
DisplayAlerts = false,
ScreenUpdating = false,
Visible = false
};
// Load the workbook template
Workbook = Excel.Workbooks.Open(#"D:\Book1.xlt");
// Execute macros that generates the report, if any
ExecuteMacros();
Workbook.SaveAs(#"D:\Ray'sTesting.xls", XlFileFormat.xlExcel8);
QuitExcel();
}
private void QuitExcel()
{
if (Workbook != null)
{
Workbook.Close(false);
Marshal.ReleaseComObject(Workbook);
}
if (Excel != null)
{
Excel.Quit();
Marshal.ReleaseComObject(Excel);
}
}
private void ExecuteMacros()
{
const string legacyModuleName = "Module1";
const string legacyMacroName = "myMacro";
bool legacyMacroExists = false;
try
{
var legacyMacroModule = Workbook.VBProject.VBComponents.Item(legacyModuleName);
if (legacyMacroModule != null)
{
int legacyMacroStartLine = legacyMacroModule.CodeModule.ProcStartLine[legacyMacroName, Microsoft.Vbe.Interop.vbext_ProcKind.vbext_pk_Proc];
legacyMacroExists = legacyMacroStartLine > 0;
}
}
catch (Exception)
{
legacyMacroExists = false;
}
if (!legacyMacroExists)
{
return;
}
// VBA code for the dynamic macro that calls the CI2 legacy macro
var moduleCode = new StringBuilder();
moduleCode.AppendLine("Public Sub LaunchLegacyMacro()");
moduleCode.AppendLine(string.Format("{0}.{1}", legacyModuleName, legacyMacroName));
moduleCode.AppendLine("End Sub");
// Add the dynamic macro to the ThisWorkbook module
var workbookMainModule = Workbook.VBProject.VBComponents.Item("ThisWorkbook");
workbookMainModule.CodeModule.AddFromString(moduleCode.ToString());
// Execute the dynamic macro
Microsoft.VisualBasic.Interaction.CallByName(Workbook, "LaunchLegacyMacro", Microsoft.VisualBasic.CallType.Method, new object[] { });
}
}
I got this working by editing the registry every time we run the excel macro by
private static void ModifyExcelSecuritySettings()
{
// Make sure we have programmatic access to the project to run macros
using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(#"Software\Microsoft\Office\14.0\Excel\Security", true))
{
if (key != null)
{
if ((int)key.GetValue("AccessVBOM", 0) != 1)
{
key.SetValue("AccessVBOM", 1);
}
key.Close();
}
}
}
So the code should look like this
public void RunReport()
{
ModifyExcelSecuritySettings();
// Launch Excel on the server
Excel = new Application
{
DisplayAlerts = false,
ScreenUpdating = false,
Visible = false
};
.....
I also created a blog post for the full solution which you can view here
http://anyrest.wordpress.com/2012/06/22/programmatic-execution-excel-macro-on-remote-machine-from-a-website/
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.