I am working on a tool which will automate a couple of actions for me. One of these actions is download an excel file, run a macro on it and then mail the file. In some cases i want the tool to just run the excel macro, then after x seconds exit excel and mail the file. This all works fine since i can just wait for fixed period of time. However, in some cases i want the to be able to check and change the data which has been retrieved by the excel macro. When this is the case i want to keep excel open until the used manually closes excel. When i detect excel is no longer opened i want to mail the file.
This last case gives me some trouble. Because i use the excel interlop to open excel, i cannot i am not able to use the WaitForExit() like i can with a normal process. When i manually close excel the process also keeps running in the process explorer.
I have searched for solutions on the internet but none of them really work. Is there any way i can achieve this in a simple way?
UPDATE
Thanks a lot for your reply, It really helped me. Waiting for excel now works correcly. I used the following code:
if (Settings.ExitExcel)
{
System.Threading.Thread.Sleep(Settings.ExcelTimeout * 1000);
//Close Excel
excelWorkbook.Close();
excelApp.Quit();
}
else
{
Excel.AppEvents_WorkbookBeforeCloseEventHandler Event_BeforeBookClose;
Event_BeforeBookClose = new Excel.AppEvents_WorkbookBeforeCloseEventHandler(WorkbookBeforeClose);
excelApp.WorkbookBeforeClose += Event_BeforeBookClose;
//Wait until excel is closed
while (!isClosed)
{
Thread.Sleep(1000);
}
//Show message
MessageBox.Show("excel closed");
}
//Clean up excel.
Marshal.FinalReleaseComObject(excelSheets);
Marshal.FinalReleaseComObject(excelWorksheet);
Marshal.FinalReleaseComObject(excelWorkbook);
Marshal.FinalReleaseComObject(excelApp);
excelApp = null;
excelWorkbook = null;
excelWorksheet = null;
excelSheets = null;
GC.Collect();
Only problem now is that excel still keeps running in the processes. Am i not closing it correctly?
You can do this by using the BeforeClose event. Basically, you need to do something along the following lines:
- Create a variable to track state i.e. If Excel is open or closed.
- Create an event handler for the WorkbookBeforeClose event.
- In the handler, check if workbook was closed and update the value of your state tracking variable accordingly.
- If the workbook is closed, perform your mail sending action.
Code can be something like this:
//Outside main
private static bool isClosed = false;
....
//Let's say your Workbook object is named book
book.WorkbookBeforeClose += new Excel.AppEvents_WorkbookBeforeCloseEventHandler(app_WorkbookBeforeClose);
if(isClosed)
sendMailMethod();
...
private static void app_WorkbookBeforeClose(Excel.Workbook wb, ref bool cancel)
{
closed = !cancel;
}
This approach does not require you to kill the process first. However, you should release the COM objects and end the process once the Excel work is done.
EDIT: To close the process, try the Application.Quit() method.
Answer:
If you have not found a solution, here is what I’ve put together that works for me.
private async void OnOpenWorkSheetImportCommand(object sender, ExecutedRoutedEventArgs e)
{
try
{
if (e.Parameter is IWorkSheetImportViewModel model)
{
if (WorksheetDictionaries.OfType<WorksheetDictionary>().FirstOrDefault(a => a.Header.Equals(model.Header)) is WorksheetDictionary worksheetDictionary)
{
Type officeType = Type.GetTypeFromProgID("Excel.Application", true);
if (officeType != null)
{
BeforeWorkbookDictionary = worksheetDictionary.WorkbookDictionary;
OpenInteropWorkbookDictionary(worksheetDictionary.WorkbookDictionary, worksheetDictionary, true);
}
else
{
throw new ArgumentException("Microsoft Excel is not installed!");
}
}
}
}
...
..
.
}
Calling OpenInteropWorkbookDictionary is where I setup Microsoft.Office.Interop.Excel
private void OpenInteropWorkbookDictionary(WorkbookDictionary workbookDictionary, WorksheetDictionary worksheetDictionary, bool activateWorksheet = false)
{
if (ExcelApplication != null && ExcelApplication.Visible == true)
{
ExcelApplication.ActiveWindow.Activate();
IntPtr handler = FindWindow(null, ExcelApplication.Caption);
SetForegroundWindow(handler);
}
else
{
if (ExcelApplication != null) Marshal.FinalReleaseComObject(ExcelApplication);
if (ExcelWorkbook != null) Marshal.FinalReleaseComObject(ExcelWorkbook);
if (ExcelWorksheet != null) Marshal.FinalReleaseComObject(ExcelWorksheet);
if (ExcelWorksheets != null) Marshal.FinalReleaseComObject(ExcelWorksheets);
ExcelApplication = null;
ExcelWorkbook = null;
ExcelWorksheet = null;
ExcelWorksheets = null;
ExcelApplication = new Microsoft.Office.Interop.Excel.Application
{
// if you want to make excel visible to user, set this property to true, false by default
Visible = true,
WindowState = Microsoft.Office.Interop.Excel.XlWindowState.xlMaximized
};
ExcelApplication.WindowActivate += new Microsoft.Office.Interop.Excel.AppEvents_WindowActivateEventHandler(OnWindowActivate);
ExcelApplication.WindowDeactivate += new Microsoft.Office.Interop.Excel.AppEvents_WindowDeactivateEventHandler(OnWindowDeactivate);
ExcelApplication.WorkbookOpen += new Microsoft.Office.Interop.Excel.AppEvents_WorkbookOpenEventHandler(OnWorkbookOpen);
ExcelApplication.WorkbookActivate += new Microsoft.Office.Interop.Excel.AppEvents_WorkbookActivateEventHandler(OnWorkbookActivate);
ExcelApplication.WorkbookDeactivate += new Microsoft.Office.Interop.Excel.AppEvents_WorkbookDeactivateEventHandler(OnWorkbookDeactivate);
// open an existing workbook
ExcelWorkbook = ExcelApplication.Workbooks.Open(BeforeWorkbookDictionary.FilePath, 0, false, 5, "", "", false, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "", true, false, 0, true, false, false);
ExcelWorkbook.BeforeClose += new Microsoft.Office.Interop.Excel.WorkbookEvents_BeforeCloseEventHandler(OnBeforeClose);
ExcelWorkbook.AfterSave += new Microsoft.Office.Interop.Excel.WorkbookEvents_AfterSaveEventHandler(OnAfterSave);
ExcelWorkbook.BeforeSave += new Microsoft.Office.Interop.Excel.WorkbookEvents_BeforeSaveEventHandler(OnBeforeSave);
// SheetEvents
ExcelWorkbook.SheetChange += new Microsoft.Office.Interop.Excel.WorkbookEvents_SheetChangeEventHandler(OnSheetChange);
ExcelWorkbook.SheetSelectionChange += new Microsoft.Office.Interop.Excel.WorkbookEvents_SheetSelectionChangeEventHandler(OnSheetSelectionChange);
ExcelWorkbook.NewSheet += new Microsoft.Office.Interop.Excel.WorkbookEvents_NewSheetEventHandler(OnNewSheet);
ExcelWorkbook.SheetBeforeDelete += new Microsoft.Office.Interop.Excel.WorkbookEvents_SheetBeforeDeleteEventHandler(OnSheetBeforeDelete);
ExcelWorkbook.SheetActivate += new Microsoft.Office.Interop.Excel.WorkbookEvents_SheetActivateEventHandler(OnSheetActivate);
ExcelWorkbook.SheetDeactivate += new Microsoft.Office.Interop.Excel.WorkbookEvents_SheetDeactivateEventHandler(OnSheetDeactivate);
//// get all sheets in workbook
ExcelWorksheets = ExcelWorkbook.Worksheets;
}
if (activateWorksheet)
{
ExcelWorksheet = ExcelWorkbook.Sheets[worksheetDictionary.Name];
ExcelWorksheet.Activate();
}
}
TO NOTICE:
- ExcelWorkbook.BeforeClose is called first then,
- ExcelApplication.WorkbookDeactivate is called last before Excel is completely closed
- So in ExcelApplication.WorkbookDeactivate is where I setup and use Process to wait for it to terminate on its own naturally.
- Extra: I use Reactive to catch the OnClose Event.
- IMPORTANT NOTE: I close Excel when the user closes the Workbook!
So in OnWorkbookDeactivate:
private async void OnWorkbookDeactivate(Microsoft.Office.Interop.Excel.Workbook Wb)
{
try
{
ProgressController = default(ProgressDialogController);
if (ExcelApplication != null && ExcelApplication.Visible == true)
{
int excelProcessId = -1;
GetWindowThreadProcessId(new IntPtr(ExcelApplication.Hwnd), ref excelProcessId);
Process ExcelProc = Process.GetProcessById(excelProcessId);
if (ExcelProc != null)
{
ExitedEventArgs = Observable.FromEventPattern<object, EventArgs>(ExcelProc, MethodParameter.Exited);
DisposableExited = ExitedEventArgs.Subscribe(evt => OnExitedEvent(evt.Sender, evt.EventArgs));
ExcelProc.EnableRaisingEvents = true;
ProgressController = await _dialogCoordinator.ShowProgressAsync(this, string.Format("Workbook {0}", BeforeWorkbookDictionary.ExcelName), "Please wait! Processing any changes...");
ProgressController.SetIndeterminate();
ExcelApplication.Quit();
if (ExcelApplication != null) Marshal.FinalReleaseComObject(ExcelApplication);
if (ExcelWorkbook != null) Marshal.FinalReleaseComObject(ExcelWorkbook);
if (ExcelWorksheet != null) Marshal.FinalReleaseComObject(ExcelWorksheet);
if (ExcelWorksheets != null) Marshal.FinalReleaseComObject(ExcelWorksheets);
ExcelApplication = null;
ExcelWorkbook = null;
ExcelWorksheet = null;
ExcelWorksheets = null;
ExcelProc.WaitForExit();
ExcelProc.Refresh();
}
}
}
catch (Exception msg)
{
...
..
.
}
}
I setup a couple things in-class to get all of this to work properly:
#region DllImports
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr hwnd, ref int lpdwProcessId);
#endregion
And:
#region RoutedEventArgs Properties
private IDisposable disposableClosingEvent;
public IDisposable DisposableClosingEvent
{
get => this.disposableClosingEvent; set => SetProperty(ref disposableClosingEvent, value, nameof(DisposableClosingEvent));
}
private IObservable<EventPattern<object, EventArgs>> exitedEventArgs;
public IObservable<EventPattern<object, EventArgs>> ExitedEventArgs
{
get => exitedEventArgs; set => SetProperty(ref exitedEventArgs, value, nameof(ExitedEventArgs));
}
private IDisposable disposableExited;
public IDisposable DisposableExited
{
get => this.disposableExited; set => SetProperty(ref disposableExited, value, nameof(DisposableExited));
}
#endregion
And Finally: The OnExitedEvent to do some last minute processing.
private async void OnExitedEvent(object sender, EventArgs e)
{
try
{
DisposableExited.Dispose();
using (SpreadsheetDocument s = SpreadsheetDocument.Open(SelectedWorkbookDictionaryImport.FilePath, false))
{
foreach (Sheet workbookSheet in s.WorkbookPart.Workbook.Sheets)
{
if (WorkbookDictionaryImports.OfType<WorkbookDictionary>().FirstOrDefault(d => d.ExcelName.Equals(SelectedWorkbookDictionaryImport.ExcelName)) is WorkbookDictionary workbookDictionary)
{
if (workbookDictionary.WorksheetDictionaryItems.OfType<WorksheetDictionary>().FirstOrDefault(d => d.Id.Equals(workbookSheet.Id)) == null)
{
Application.Current.Dispatcher.Invoke(() =>
{
WorksheetDictionary worksheetDictionary = new WorksheetDictionary()
{
WorksheetDictionaryId = Guid.NewGuid().ToString(),
IsSelected = false,
Id = workbookSheet.Id,
SheetId = workbookSheet.SheetId,
Name = workbookSheet.Name,
Header = workbookSheet.Name,
HeaderInfo = GetSheetStateValues(workbookSheet.State),
Code = GetSheetCodeValues(workbookSheet.Name.ToString()).ToString(),
Description = BeforeWorkbookDictionary.Subject,
WorkbookDictionary = BeforeWorkbookDictionary
};
workbookDictionary.WorksheetDictionaryItems.Add(worksheetDictionary);
});
}
}
}
}
}
catch (Exception msg)
{
...
..
.
}
finally
{
if (ProgressController != null)
{
await ProgressController.CloseAsync();
ProgressController = default(ProgressDialogController);
}
}
}
Hope this will help you and others, of coarse make changes to fit your processing needs.