Excel automation: Close event missing - c#

Another hi all,
I am doing Excel automation via Interop in C#, and I want to be informed when a workbook is closed. However, there is no Close event on the workbook nor a Quit event on the application.
Has anybody done that before? How can I write a piece of code which reacts to the workbook being closed (which is only executed if the workbook is really closed)? Ideally that should happen after closing the workbook, so I can rely on the file to reflect all changes.
Details about what I found so far:
There is a BeforeClose() event, but if there are unsaved changes this event is raised before the user being asked whether to save them, so at the moment I can process the event, I don't have the final file and I cannot release the COM objects, both things that I need to have/do. I do not even know whether the workbook will actually be closed, since the user might choose to abort closing.
Then there is a BeforeSave() event. So, if the user chooses "Yes" to save unsaved changes, then BeforeSave() is executed after BeforeClose(). However, if the user chooses to "Abort", then hits "file->save", the exact same order of events is executed. Further, if the user chooses "No", the BeforeSave() isn't executed at all. The same holds as long as the user doesn't click any of these options.

I've created a hack using a polling-like approach, and it works:
Given the workbook to observe, I create a thread which periodically tries to find that workbook in the workbooks collection.
(The DisposableCom class is my current solution to properly cleanup COM objects.)
Excel.Application app = wbWorkbook.Application;
string sWorkbookName = wbWorkbook.Name;
Thread overseeWorkbooksThread = new Thread(new ThreadStart(
delegate()
{
bool bOpened = false;
Excel.Workbooks wbsWorkbooks = app.Workbooks;
using (new DisposableCom<Excel.Workbooks>(wbsWorkbooks))
{
while (true)
{
Thread.Sleep(1000);
if (wbsWorkbooks.ContainsWorkbookProperly(sWorkbookName))
bOpened = true;
else
if (bOpened)
// Workbook was open, so it has been closed.
break;
else
{
// Workbook simply not finished opening, do nothing
}
}
// Workbook closed
RunTheCodeToBeRunAfterWorkbookIsClosed();
}
}));
overseeWorkbooksThread.Start();
The "ContainsWorkbookProperly" extension methods looks like this:
public static bool ContainsWorkbookProperly(this Excel.Workbooks excelWbs,
string sWorkbookName)
{
Excel.Workbook wbTemp = null;
try
wbTemp = excelWbs.Item(sWorkbookName);
catch (Exception)
{
// ignore
}
if (wbTemp != null)
{
new DisposableCom<Excel.Workbook>(wbTemp).Dispose();
return true;
}
return false;
}
Still I would be interested if there is a simpler or better solution.

This is not my code, but this worked a treat for me:
https://gist.github.com/jmangelo/301884
Copy paste:
using System;
using Excel = Microsoft.Office.Interop.Excel;
namespace Helpers.Vsto
{
public sealed class WorkbookClosedMonitor
{
internal class CloseRequestInfo
{
public CloseRequestInfo(string name, int count)
{
this.WorkbookName = name;
this.WorkbookCount = count;
}
public string WorkbookName { get; set; }
public int WorkbookCount { get; set; }
}
public WorkbookClosedMonitor(Excel.Application application)
{
if (application == null)
{
throw new ArgumentNullException("application");
}
this.Application = application;
this.Application.WorkbookActivate += Application_WorkbookActivate;
this.Application.WorkbookBeforeClose += Application_WorkbookBeforeClose;
this.Application.WorkbookDeactivate += Application_WorkbookDeactivate;
}
public event EventHandler<WorkbookClosedEventArgs> WorkbookClosed;
public Excel.Application Application { get; private set; }
private CloseRequestInfo PendingRequest { get; set; }
private void Application_WorkbookDeactivate(Excel.Workbook wb)
{
if (this.Application.Workbooks.Count == 1)
{
// With only one workbook available deactivating means it will be closed
this.PendingRequest = null;
this.OnWorkbookClosed(new WorkbookClosedEventArgs(wb.Name));
}
}
private void Application_WorkbookBeforeClose(Excel.Workbook wb, ref bool cancel)
{
if (!cancel)
{
this.PendingRequest = new CloseRequestInfo(
wb.Name,
this.Application.Workbooks.Count);
}
}
private void Application_WorkbookActivate(Excel.Workbook wb)
{
// A workbook was closed if a request is pending and the workbook count decreased
bool wasWorkbookClosed = true
&& this.PendingRequest != null
&& this.Application.Workbooks.Count < this.PendingRequest.WorkbookCount;
if (wasWorkbookClosed)
{
var args = new WorkbookClosedEventArgs(this.PendingRequest.WorkbookName);
this.PendingRequest = null;
this.OnWorkbookClosed(args);
}
else
{
this.PendingRequest = null;
}
}
private void OnWorkbookClosed(WorkbookClosedEventArgs e)
{
var handler = this.WorkbookClosed;
if (handler != null)
{
handler(this, e);
}
}
}
public sealed class WorkbookClosedEventArgs : EventArgs
{
internal WorkbookClosedEventArgs(string name)
{
this.Name = name;
}
public string Name { get; private set; }
}
}
When I used it I changed it from return the name of the workbook to a reference to the workbook.

Schedule a SyncContext action on workbook.Deactivate. This event is fired both when the workbook is closed and when another workbook takes focus.
Normally, in the Deactivate handler you can't check if the workbook is closed or just lost focus, but you can enqueue an action on the SyncContext to execute right after the event. In that action you can check if your workbook is still alive and execute code in case it's not.
Here's an example:
// put a syncContext instance somewhere you can reach it
static SynchronizationContext syncContext = SynchronizationContext.Current ?? new System.Windows.Forms.WindowsFormsSynchronizationContext();
// subscribe to workbook deactivate
workbook.Deactivate += workbook_Deactivate;
[DebuggerHidden]
private void workbook_Deactivate()
{
// here, the workbook is still alive, but we can schedule
// an action via the SyncContext which will execute
// right after the deactivate event is completed. At that
// point, the workbook instance (RCW) will no longer be usable
// meaning that the workbook has been closed
syncContext.Post(x =>
{
try
{
// will throw if workbook is gone
workbook.Path.ToString();
}
catch
{
// handle workbook closed
}
}, null);
}

Can you use both events? On BeforeClose() set a flag, then BeforeSave() see if the flag is set. You would need a way to reset it, though, in case BeforeClose() is triggered and BeforeSave() isn't. Not sure if there is something else that could help with that.
Edit: Looks like you covered this already with "the exact same order of events is executed". But if you can find a way to reset it (another "Cancel" event?) it may work.

Related

Capture Button Click event inside a MessageBox in another application

I want to capture the OK Button's Click event on a MessageBox shown by another WinForms application.
I want to achieve this using UI Automation. After some research, I have found that IUIAutomation::AddAutomationEventHandler will do the work for me.
Though, I can capture the Click event of any other button, I'm unable to capture a Click event of the MessageBox.
My code is as follows:
var FindDialogButton = appElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "OK"));
if (FindDialogButton != null)
{
if (FindDialogButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
{
Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, FindDialogButton, TreeScope.Element, new AutomationEventHandler(DialogHandler));
}
}
private void DialogHandler(object sender, AutomationEventArgs e)
{
MessageBox.Show("Dialog Button clicked at : " + DateTime.Now);
}
EDIT:
My Complete code is as follows:
private void DialogButtonHandle()
{
AutomationElement rootElement = AutomationElement.RootElement;
if (rootElement != null)
{
System.Windows.Automation.Condition condition = new PropertyCondition
(AutomationElement.NameProperty, "Windows Application"); //This part gets the handle of the Windows application that has the MessageBox
AutomationElement appElement = rootElement.FindFirst(TreeScope.Children, condition);
var FindDialogButton = appElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "OK")); // This part gets the handle of the button inside the messagebox
if (FindDialogButton != null)
{
if (FindDialogButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
{
Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, FindDialogButton, TreeScope.Element, new AutomationEventHandler(DialogHandler)); //Here I am trying to catch the click of "OK" button inside the MessageBox
}
}
}
}
private void DialogHandler(object sender, AutomationEventArgs e)
{
//On Button click I am trying to display a message that the button has been clicked
MessageBox.Show("MessageBox Button Clicked");
}
I tried to keep this procedure as generic as possible, so that it will work whether the application you're watching is already running when your app is started or not.
You just need to provide the watched Application's Process Name or its Main Window Title to let the procedure identify this application.
Use one of these Fields and the corresponding Enumerator:
private string appProcessName = "theAppProcessName"; and
FindWindowMethod.ProcessName
// Or
private string appWindowTitle = "theAppMainWindowTitle"; and
FindWindowMethod.Caption
passing these values to the procedure that starts the watcher, e.g., :
StartAppWatcher(appProcessName, FindWindowMethod.ProcessName);
As you can see - since you tagged your question as winforms - this is a complete Form (named frmWindowWatcher) that contains all the logic required to perform this task.
How does it work:
When you start frmWindowWatcher, the procedure verifies whether the watched application (here, identified using its Process name, but you can change the method, as already described), is already running.
If it is, it initializes a support class, ElementWindow, which will contain some informations about the watched application.
I added this support class in case you need to perform some actions if the watched application is already running (in this case, the ElementWindow windowElement Field won't be null when the StartAppWatcher() method is called). These informations may also be useful in other cases.
When a new Windows is opened in the System, the procedure verifies whether this Window belongs to the watched application. If it does, the Process ID will be the same. If the Windows is a MessageBox (identified using its standard ClassName: #32770) and it belongs to the watched Application, an AutomationEventHandler is attached to the child OK Button.
Here, I'm using a Delegate: AutomationEventHandler DialogButtonHandler for the handler and an instance Field (AutomationElement msgBoxButton) for the Button Element, because these references are needed to remove the Button Click Handler when the MessageBox is closed.
When the MessageBox's OK Button is clicked, the MessageBoxButtonHandler method is called. Here, you can determine which action to take at this point.
When the frmWindowWatcher Form is closed, all Automation Handlers are removed, calling the Automation.RemoveAllEventHandlers() method, to provide a final clean up and prevent your app from leaking resources.
using System.Diagnostics;
using System.Linq;
using System.Windows.Automation;
using System.Windows.Forms;
public partial class frmWindowWatcher : Form
{
AutomationEventHandler DialogButtonHandler = null;
AutomationElement msgBoxButton = null;
ElementWindow windowElement = null;
int currentProcessId = 0;
private string appProcessName = "theAppProcessName";
//private string appWindowTitle = "theAppMainWindowTitle";
public enum FindWindowMethod
{
ProcessName,
Caption
}
public frmWindowWatcher()
{
InitializeComponent();
using (var proc = Process.GetCurrentProcess()) {
currentProcessId = proc.Id;
}
// Identify the application by its Process name...
StartAppWatcher(appProcessName, FindWindowMethod.ProcessName);
// ... or by its main Window Title
//StartAppWatcher(appWindowTitle, FindWindowMethod.Caption);
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
Automation.RemoveAllEventHandlers();
base.OnFormClosed(e);
}
private void StartAppWatcher(string elementName, FindWindowMethod method)
{
windowElement = GetAppElement(elementName, method);
// (...)
// You may want to perform some actions if the watched application is already running when you start your app
Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement,
TreeScope.Subtree, (elm, e) => {
AutomationElement element = elm as AutomationElement;
try
{
if (element == null || element.Current.ProcessId == currentProcessId) return;
if (windowElement == null) windowElement = GetAppElement(elementName, method);
if (windowElement == null || windowElement.ProcessId != element.Current.ProcessId) return;
// If the Window is a MessageBox generated by the watched app, attach the handler
if (element.Current.ClassName == "#32770")
{
msgBoxButton = element.FindFirst(TreeScope.Descendants,
new PropertyCondition(AutomationElement.NameProperty, "OK"));
if (msgBoxButton != null && msgBoxButton.GetSupportedPatterns().Any(p => p.Equals(InvokePattern.Pattern)))
{
Automation.AddAutomationEventHandler(
InvokePattern.InvokedEvent, msgBoxButton, TreeScope.Element,
DialogButtonHandler = new AutomationEventHandler(MessageBoxButtonHandler));
}
}
}
catch (ElementNotAvailableException) {
// Ignore: this exception may be raised if you show a modal dialog,
// in your own app, that blocks the execution. When the dialog is closed,
// AutomationElement element is no longer available
}
});
Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, AutomationElement.RootElement,
TreeScope.Subtree, (elm, e) => {
AutomationElement element = elm as AutomationElement;
if (element == null || element.Current.ProcessId == currentProcessId || windowElement == null) return;
if (windowElement.ProcessId == element.Current.ProcessId) {
if (windowElement.MainWindowTitle == element.Current.Name) {
windowElement = null;
}
}
});
}
private void MessageBoxButtonHandler(object sender, AutomationEventArgs e)
{
Console.WriteLine("Dialog Button clicked at : " + DateTime.Now.ToString());
// (...)
// Remove the handler after, since the next MessageBox needs a new handler.
Automation.RemoveAutomationEventHandler(e.EventId, msgBoxButton, DialogButtonHandler);
}
private ElementWindow GetAppElement(string elementName, FindWindowMethod method)
{
Process proc = null;
try {
switch (method) {
case FindWindowMethod.ProcessName:
proc = Process.GetProcessesByName(elementName).FirstOrDefault();
break;
case FindWindowMethod.Caption:
proc = Process.GetProcesses().FirstOrDefault(p => p.MainWindowTitle == elementName);
break;
}
return CreateElementWindow(proc);
}
finally {
proc?.Dispose();
}
}
private ElementWindow CreateElementWindow(Process process) =>
process == null ? null : new ElementWindow(process.ProcessName) {
MainWindowTitle = process.MainWindowTitle,
MainWindowHandle = process.MainWindowHandle,
ProcessId = process.Id
};
}
Support class, used to store informations on the watched application:
It's initialized using the App's Process Name:
public ElementWindow(string processName)
but of course you can change it as required, using the Window Title as described before, or even remove the initialization's argument if you prefer (the class just need to not be null when the watched Application has been detected and identified).
using System.Collections.Generic;
public class ElementWindow
{
public ElementWindow(string processName) => this.ProcessName = processName;
public string ProcessName { get; set; }
public string MainWindowTitle { get; set; }
public int ProcessId { get; set; }
public IntPtr MainWindowHandle { get; set; }
}

Cannot call Close when Closing event is override

In my MainWindow constructor I ovverided the Closing event because I need to call another method that perform some task, like:
public MainWindow()
{
InitializeComponent();
Closing += (x, y) =>
{
y.Cancel = true;
_discard = true;
CheckSettings();
};
}
public void CheckSettings(bool x)
{
if(x)
Close();
}
on the Close line I get:
cannot set visibility or call show or showdialog after window has closed
why??
(as requested in you comment...)
You cannot call Close from a Closing event handler.
If the logic determining if the form can be closed is implemented in CheckSettings:
public MainWindow()
{
InitializeComponent();
Closing += (sender, args) =>
{
args.Cancel = !CheckSettings();
...
};
}
public bool CheckSettings()
{
// Check and return true if the form can be closed
}
Until you return from your event handler (that's made the call to CheckSettings), the UI framework you're using may not evaluate the content of the EventArgs that you've named as y and set Cancel = true on.
If you're using WPF, for example, the Close method eventually calls down into another method called VerifyNotClosing (via InternalClose) which at the time of writing looks like this:
private void VerifyNotClosing()
{
if (_isClosing == true)
{
throw new InvalidOperationException(SR.Get(SRID.InvalidOperationDuringClosing));
}
if (IsSourceWindowNull == false && IsCompositionTargetInvalid == true)
{
throw new InvalidOperationException(SR.Get(SRID.InvalidCompositionTarget));
}
}
The relevant bit there is the first if that checks a member variable called _isClosing and throws an exception if the form is in the process of closing.
The InternalClose method reacts to the state of the Cancel property of the EventArgs after the event handlers have been called:
CancelEventArgs e = new CancelEventArgs(false);
try
{
// The event handler is called here
OnClosing(e);
}
catch
{
CloseWindowBeforeShow();
throw;
}
// The status of the .Cancel on the EventArgs is not checked until here
if (ShouldCloseWindow(e.Cancel))
{
CloseWindowBeforeShow();
}
else
{
_isClosing = false;
// 03/14/2006 -- hamidm
// WOSB 1560557 Dialog does not close with ESC key after it has been cancelled
//
// No need to reset DialogResult to null here since source window is null. That means
// that ShowDialog has not been called and thus no need to worry about DialogResult.
}
The code above (from the InternalClose method) is after the call to VerifyNotClosing which is why the subsequent call to Close, before the first one has finished, results in the exception being thrown.

Multiple thread not working correctly in C#

I create parallel process and DataTable dtUser have two rows, it should create two browser:
Parallel.ForEach(dtUser.AsEnumerable(), items =>
OpenBrowser(items["user"].ToString(), items["pass"].ToString()));
Lapsoft_OneDriver browser;
public void OpenBrowser(string username, string password)
{
browser = new Lapsoft_OneDriver(Browsers.Chrome);
browser.GoToUrl(link);
browser.FindElementById("txtUserName").SendKeys(username);
browser.FindElementById("txtpassword").SendKeys(password);
}
It create two Chrome process but only first process running line code block:
browser.GoToUrl(link);
browser.FindElementById("txtUserName").SendKeys(username);
browser.FindElementById("txtpassword").SendKeys(password);
The second process only initializes new browser and not do anything.
If I change this line:
browser = new Lapsoft_OneDriver(Browsers.Chrome);
to
var browser = new Lapsoft_OneDriver(Browsers.Chrome);
It's working.
But another method continues to use variable browser to execute other code.
So, I must declare global variable Lapsoft_OneDriver browser out of a function to use in another method use it.
My problem is:
Why using Lapsoft_OneDriver browser; it create two Chrome process but only first process active, it will insert to browser.FindElementById("txtUserName") two values of variable username and second process not do anything?
Updated:
When to change the code, I have any problem.
I will add more code of frmMain_Load:
private void frmMain_Load(object sender, EventArgs e)
{
thread = new LThread();
thread.StartedEvent += new LThread.startDelegate(AllCaseProgram);
numLog = int.Parse(dtSetting.Rows[0]["num_Log"].ToString());
}
int numProcess;
private void AllCaseProgram(object args)
{
try
{
switch (numProcess)
{
case 0:
Parallel.ForEach(dtUser.AsEnumerable(), items => Start(items["user"].ToString(), items["pass"].ToString()));
break;
case 1:
ClickCart();
break;
case 2:
Result();
break;
}
}
catch (Exception ex)
{
if (browser != null)
browser.Cleanup();
numProcess = 0;
AllCaseProgram(null);
}
}
At event of button StartProgram()_Click. I start Thread like: thread.Start();
You said: should be add this function to my program.
public static void Start(string user, string pwd)
{
var test = new frmMain();
test.OpenBrowser(user, pwd);
test.ClickCart();
}
My update question is:
Seem function Start(string user, string pwd) should be change to function AllCaseProgram include all switch case.
And variable numLog in frmMain_Load have values = 3. In function test.ClickCart() I also use this variable but values auto change to 0.
Have any issues with code? Thanks.
And LThread class is:
public class LThread : BackgroundWorker
{
#region Members
public delegate void startDelegate(string ID);
public event startDelegate StartedEvent;
private static int RandNumber(int Low, int High)
{
Random rndNum = new Random(int.Parse(Guid.NewGuid().ToString().Substring(0, 8), System.Globalization.NumberStyles.HexNumber));
int rnd = rndNum.Next(Low, High);
return rnd;
}
protected override void OnDoWork(DoWorkEventArgs e)
{
StartedEvent(RandNumber(100,10000).ToString()); //put whatever parameter suits you or nothing
base.OnDoWork(e);
e.Result = e.Argument;
}
BackgroundWorker bwThread;
// Main thread sets this event to stop worker thread:
public Boolean bwIsRun;
int m_time_delay = 10000;
Delegate m_form_method_run;
Delegate m_form_method_stop;
Form m_type_form;
#endregion
#region Functions
public void Start()
{
try
{
bwIsRun = true;
this.RunWorkerAsync();
}
catch { }
}
public void Stop()
{
try
{
bwIsRun = false;
}
catch { }
}
private void StartToListen(object sender, DoWorkEventArgs e)
{
while (true)
{
Thread.Sleep(m_time_delay);
if (bwIsRun == true)
{
m_type_form.Invoke(m_form_method_run);
}
else
{
BackgroundWorker bwAsync = sender as BackgroundWorker;
if (bwAsync.CancellationPending)
{
e.Cancel = true;
return;
}
break;
}
}
}
#endregion
}
You should encapsulate your state for each test run. That way you'll have a class that has the responsibility the start a browser, execute one or more actions, while keeping all the required state belonging to a single run private for just one instance, while you can have a many instances as you like (if resources permit).
// this is NOT a winform, this is a new and seperate class ...
// don't try to mix this with an WinForm, that will fail
public class BrowserTestRunner
{
// only this Test instances uses this browser
Lapsoft_OneDriver browser;
private void OpenBrowser(string username, string password)
{
browser = new Lapsoft_OneDriver(Browsers.Chrome);
browser.GoToUrl(link);
browser.FindElementById("txtUserName").SendKeys(username);
browser.FindElementById("txtpassword").SendKeys(password);
// you probably want to click on something here
}
// some other test
private void ClickCart()
{
browser.FindElementById("btnCart").Click();
}
// add other actions here
// this starts the test for ONE browser
public static void Start(string user, string pwd)
{
var runner = new BrowserTestRunner();
runner.OpenBrowser(user, pwd);
// wait for stuff, check data, prepare the next steps
// for example
// runner.ClickCart();
// other actons here
}
}
Now you can create as many Test class instances as you like, while each instance of the class manages its own internal state, without interfering with other instances:
Parallel.ForEach(dtUser.AsEnumerable(), items =>
BrowserTestRunner.Start(items["user"].ToString(), items["pass"].ToString()));
If you want to start that from your backgroundworker do:
private void AllCaseProgram(object args)
{
try
{
switch (numProcess)
{
case 0:
Parallel.ForEach(
dtUser.AsEnumerable(),
items => BrowserTestRunner.Start(items["user"].ToString(), items["pass"].ToString()));
break;
case 1:
ClickCart();
break;
case 2:
Result();
break;
}
}
catch (Exception ex)
{
if (browser != null)
browser.Cleanup();
numProcess = 0;
AllCaseProgram(null);
}
}
By all means: don't start the main form again. Just separate your WinForm from the code you use to operate the browser. That does mean that you have to move the code that interacts with the browser to the BrowserTestRunner. Don't try in keeping the logic for your selenium stuff in the WinForm class because that is doomed to fail. As you are already experiencing.
What you got here is sort of a race condition. You got two threads not getting along when handling a single field in the class. Your problem is only that you don't have sufficient space to store all the browser instances you require.
What happens is basically that the first thread enters the method, creates a instance of the chrome browser and stores it in the variable. Then the second thread enters the function and does the same thing. But it also stores the instance in the same variable. Now the first thread continues and goes to a link. But the instance it is working with is already replaced by the second thread. And so on. This may happen with the threads the other way around or the overlapping may happen after more lines where handled. But it is bound to go wrong.
The way to resolve it, is as you noticed to make the variable local by adding a var. This way both threads are working with distinct variables.
Now you said you need the variable in another function. The question is: Do you need both? Do you need only one? Do you need a specific one?
In case you need only one, you just store the variable in the global variable by adding a line like this in your function:
this.browser = browser;
So it would look like this in total:
Lapsoft_OneDriver browser;
public void OpenBrowser(string username, string password)
{
var localBrowser = new Lapsoft_OneDriver(Browsers.Chrome);
localBrowser.GoToUrl(link);
localBrowser.FindElementById("txtUserName").SendKeys(username);
localBrowser.FindElementById("txtpassword").SendKeys(password);
this.browser = localBrowser;
}
I changed the name of the local browser variable, so it gets clearer what variable is used. Do note that either one of the created browsers could end up in the variable.
In case you need a specific one you have to determine if you have the correct one and store the result after this.
If you need both you have to store them in a list. The namespace System.Collections.Concurrent offers lists that can be handled by multiple threads at once.

On ASP.NET formview when are the ModeChanged and ModeChanging events raised?

This popped up, when I was trying to find why the OnModeChanging handler wasn't being called when I called the ChangeMode event of my formview.
On the formview's ChangeMode method MSDN page , it is stated that it:
switches the FormView control to the specified data-entry mode
but also that:
the ModeChanged and ModeChanging events are not raised when this method is called
And in the ModeChanged and ModeChanging events pages, it says that they occur:
when the FormView control switches between edit, insert, and read-only mode
after/before the mode changes, respectively.
Can you explain it to me: when are the ModeChanged/ing events raised?
And, is there a way to force these events to be raised?
I think I know why now. I've found an answer in other forum, and though I didn't find the code of FormView, I've found a DetailsView implementation and I think in this case it might be similar.
Basically what I've understood of it, is that the ModeChanged/ing events are raised when command buttons are clicked (Cancel, Edit, Insert, New and Update), i.e. when one doesn't have direct control over these events, and when we use the ChangeMode method, we know that the mode has changed (or will be changed) and it would make no sense of raising an event..
DetailsView ChangeMode:
public void ChangeMode(DetailsViewMode newMode) {
Mode = newMode;
}
DetailsView command handlers:
private void HandleCancel() {
bool isBoundToDataSourceControl = IsBoundUsingDataSourceID;
DetailsViewModeEventArgs e = new DetailsViewModeEventArgs(DefaultMode, true);
OnModeChanging(e);
if (e.Cancel) {
return;
}
if (isBoundToDataSourceControl) {
Mode = e.NewMode;
OnModeChanged(EventArgs.Empty);
}
RequiresDataBinding = true;
}
private void HandleEdit() {
if (PageIndex < 0) {
return;
}
DetailsViewModeEventArgs e = new DetailsViewModeEventArgs(DetailsViewMode.Edit, false);
OnModeChanging(e);
if (e.Cancel) {
return;
}
if (IsBoundUsingDataSourceID) {
Mode = e.NewMode;
OnModeChanged(EventArgs.Empty);
}
RequiresDataBinding = true;
}
private bool HandleInsertCallback(int affectedRows, Exception ex) {
DetailsViewInsertedEventArgs dea = new DetailsViewInsertedEventArgs(affectedRows, ex);
dea.SetValues(_insertValues);
OnItemInserted(dea);
_insertValues = null;
if (ex != null && !dea.ExceptionHandled) {
if (PageIsValidAfterModelException()) {
return false;
}
dea.KeepInInsertMode = true;
}
if (!dea.KeepInInsertMode) {
DetailsViewModeEventArgs eMode = new DetailsViewModeEventArgs(DefaultMode, false);
OnModeChanging(eMode);
if (!eMode.Cancel) {
Mode = eMode.NewMode;
OnModeChanged(EventArgs.Empty);
RequiresDataBinding = true;
}
}
return true;
}
private void HandleNew() {
DetailsViewModeEventArgs e = new DetailsViewModeEventArgs(DetailsViewMode.Insert, false);
OnModeChanging(e);
if (e.Cancel) {
return;
}
if (IsBoundUsingDataSourceID) {
Mode = e.NewMode;
OnModeChanged(EventArgs.Empty);
}
RequiresDataBinding = true;
}
private bool HandleUpdateCallback(int affectedRows, Exception ex) {
DetailsViewUpdatedEventArgs dea = new DetailsViewUpdatedEventArgs(affectedRows, ex);
dea.SetOldValues(_updateOldValues);
dea.SetNewValues(_updateNewValues);
dea.SetKeys(_updateKeys);
OnItemUpdated(dea);
_updateKeys = null;
_updateOldValues = null;
_updateNewValues = null;
if (ex != null && !dea.ExceptionHandled) {
if (PageIsValidAfterModelException()) {
return false;
}
dea.KeepInEditMode = true;
}
if (!dea.KeepInEditMode) {
DetailsViewModeEventArgs eMode = new DetailsViewModeEventArgs(DefaultMode, false);
OnModeChanging(eMode);
if (!eMode.Cancel) {
Mode = eMode.NewMode;
OnModeChanged(EventArgs.Empty);
RequiresDataBinding = true;
}
}
return true;
}
With ChangeMode you are choosing that the control switch to one of it's modes.
When it starts to performing this task, the ModeChanging event is raised (to indicate that it's in progress) (optionally do something here).
Once that task is completed, it raises the ModeChanged event (to indicate that it's done) (optionally do something here).
[Updated]
I see your point. How could you consume the events if they don't get raised.
I'm going to guess at it, that they don't get raised initially because of nothing to do, just perform the changing of the mode.
In either case I guess, it's more of a state change than the raising of events.
[Updated]
I think what we are both saying is that if no one has subscribed to the event (i.e., no one is listening for it), there's no point in raising it.

Disposed Forms with a Base and threading returns null for progress bar

I have a base form that I use when calling 2 forms. Previously when calling the forms I didn't dispose of them, but I have found that reusing them, they would stay in memory and not get collected. So I have instead used a using statement instead to clear the memory, and all my problem are fixed.
But now a new problem arises, one that I had previously when testing my app with mono on Linux. I though it might be a mono specific problem, but since adding the using statement the same thing happens on my Windows machine. So it might just be that the Garbage Collector on Mono is different and was disposing properly of my forms.
Here is my problem I have a thread that I start to extract files in the background And I have progress bar telling me the progress, before using the dispose if I closed the form and reopened it my files extracted correctly and the progress bar was working fine. But now they work fine the first time, but if I reopen the form or the other one that has the same base, the extraction is not working, no files are extracted because I have a null exception when reporting the progress.
private void ExtractFiles()
{
Zip.ExtractProgress += new EventHandler<ExtractProgressArgs>(Utils_ExtractProgress);
Thread t = new Thread(new ThreadStart(Zip.ExtractZip));
t.IsBackground = true;
t.Start();
FilesExtracted = true;
}
void Utils_ExtractProgress(object sender, ExtractProgressArgs e)
{
UpdateProgress(e.Pourcentage);
}
private delegate void UpdateProgressDelegate(int Pourc);
private void UpdateProgress(int Pourc)
{
lock (this)
{
if (Progress.ProgressBar.InvokeRequired)
{
UpdateProgressDelegate del = new UpdateProgressDelegate(UpdateProgress);
Progress.ProgressBar.BeginInvoke(del, Pourc);
} else
{
Progress.Value = Pourc;
}
}
}
This code is in my BaseForm, the Progress control isn't null, but all of it's properties have null exceptions. So when checking if Invoked is required it raises an Null exception.
Here is my Zip.Extract method
public static event EventHandler<ExtractProgressArgs> ExtractProgress;
static ExtractProgressArgs Progress;
internal static void ExtractZip()
{
try
{
using (ZipFile zip = ZipFile.Read(Variables.Filename))
{
Progress = new ExtractProgressArgs();
Progress.TotalToTransfer = Convert.ToInt32(zip.Sum(e => e.UncompressedSize));
zip.ExtractProgress += new EventHandler<ExtractProgressEventArgs>(zip_ExtractProgress);
Old = 0; New = 0;
foreach (ZipEntry item in zip)
{
item.Extract(Variables.TempFolder, ExtractExistingFileAction.OverwriteSilently);
}
}
} catch (Exception)
{
}
}
static long Old;
static long New;
static void zip_ExtractProgress(object sender, ExtractProgressEventArgs e)
{
if (e.EventType == ZipProgressEventType.Extracting_EntryBytesWritten)
{
New = e.BytesTransferred;
Progress.Transferred += New - Old;
Old = e.BytesTransferred;
if (ExtractProgress != null)
{
ExtractProgress(e.CurrentEntry, Progress);
}
} else if (e.EventType == ZipProgressEventType.Extracting_AfterExtractEntry)
{
Old = 0;
}
}
Might be because my Zip.Extract is static? I have almost no knowledge of multi-threading, like synchronization, etc.
The short answer is yes, some of your problems are due to the static nature of those operations.
You should be able to resolve the problem by removing the static declarations from your Zip class and then creating an instance of it as needed.

Categories

Resources