The Application.Reset Event

riktek

Member
Local time
Today, 13:00
Joined
Dec 15, 2023
Messages
171
The Access.Application class has no events, unlike the Excel.Application class, so Access users are left to their own devices. The most critically important application behavior occurs on a project reset because this instantly and thoroughly disables an Access application, clearing all its variables and thereby destroying virtually all its runtime objects. The ability to trap this behavior and raise an event application-wide offers the opportunity for an application to become self-healing. Here’s how.

Among the runtime objects a project reset destroys are all event sources and sinks because both must be objects defined in class modules and such objects can exist only so long as pointers to them. The variables ultimately containing those pointers will survive a reset but the pointers will not. So, we must accommodate these difficulties, which will require a suitable runtime environment. It must of course be durable and lightweight, and also autostart and self heal. In the end, we will jailbreak event processing and remove it somewhat to a standard module.

Form_Unload() and Form_Close() will run after a project reset even if Class_Terminate() does not, so to trap a project reset, we create frmCanary and a Form_Unload() in its module.

We then create an application module named My to contain application properties and methods. My must be a standard module because it must be impervious to instancing so as to enforce a singleton pattern, and its procedures and variables must survive a reset.

We then create an application form, frmMain, which together with My constitute a durable application runtime environment. frmMain cannot be the application display (startup) form because it must open hidden, so we also create frmStartup and designate it as the application display form. The sole utility of any display form is its Form_Open() event procedure because it is the first event procedure that can run after application startup, and that event procedure itself must be cancelled because the form will become visible if Form_Load() is allowed to run. We thus must condition frmStartup.Form_Open()’s Cancel argument on frmMain loading successfully, as further described.

In My, we then create Main as an application property to return frmMain. We back Public Property Get Main() with a static local variable because it is identical to a private module-level variable in that it retains its value during the lifetime of its module. We also employ a self-healing object variable (SHOV) pattern, conditionally setting the variable on the procedure’s call if it is Nothing, to avoid the necessity of a Property Set procedure.

Elsewhere, we create AppIsLoaded(), a convenience function for readability to test My.Main().

Code:
    Function AppIsLoaded() As Boolean  

        AppIsLoaded = (Not (My.Main Is Nothing))

    End Function    'AppIsLoaded()

Returning to frmStartup.Form_Open(), we therein do:

Code:
Cancel = AppIsLoaded

We then create clsAppEventSource to define an application event source and therein declare an Access.Form variable named Canary, assign an instance of frmCanary to that variable in its Class_Initialize(), and destroy it in its Class_Terminate(). We also declare event Reset() and define a public OnReset() method to raise the Reset event.

Returning to My, we then create AppEventSource() as an application property, also implementing a SHOV pattern to avoid the necessity of a Property Set procedure.

We then create clsAppEventSink to define a default application event sink. Therein, we declare WithEvents a clsAppEventSource variable named oAppEventSource, assign My.AppEventSource() to it in Class_Initialize(), and clear that variable in Class_Terminate(). We go on to create oAppEventSource_Reset() to sink the event.

In frmMain, we create a public Init() and therein do:

Code:
Set My.AppEventSink = New clsAppEventSink

In frmMain.Form_Open(), we also do:

Code:
Init

Back again to My, we create OnReset() and therein do:

Code:
    If gblnUnloading Then       'Case application closing.
    '   Do nothing.  Exit gracefully without reloading.
    Else                        'Case project reset.
        Main.Init
        AppEvents.OnReset
    End If

Also in My, we create a public Boolean variable gblnUnloading and in frmMain.Form_Unload() do:

Code:
gblnUnloading = True

Finally, we return to frmCanary.Form_Unload(), and therein do:

Code:
My.OnReset

With this, the startup event progression is:
• Startup opens frmStartup.
frmStartup.Form_Open() opens frmMain by testing My.Main().
frmMain.Form_Open() calls frmMain.Init().
frmMain.Init() initializes clsAppEventSink and assigns it to My.AppEventSink.
clsAppEventSink.Class_Initialize() assigns My.AppEventSource() to clsAppEventSink.oAppEventSource.
My.AppEventSource() initializes clsAppEventSource and returns an instance to clsAppEventSink.Class_Initialize().
clsAppEventSource.Class_Initialize() initializes frmCanary and assigns it to clsAppEventSource.Canary.

The Reset event progression is:
• A project reset destroys the clsAppEventSource instance and its Canary variable.
frmCanary.Form_Unload() calls My.OnReset(), which is unaffected by the reset because it is in a standard module.
My.OnReset() calls frmMain.Init() via My.Main().
My.Main() reinitializes its static form variable lfrmMain.
frmMain.Init() reinitializes My.AppEventSink.
clsAppEventSink.Class_Initialize() calls My.AppEventSource().
My.AppEventSource() reinitializes clsAppEventSource.
clsAppEventSource.Class_Initialize() reinitializes frmCanary.
My.OnReset() calls My.AppEventSource.OnReset().
clsAppEventSource.OnReset() raises clsAppEventSource.Reset().
• The clsAppEventSink instance assigned to My.AppEventSink sinks and handles clsAppEventSource.Reset().

The shutdown event progression is:
frmMain.Form_Unload() is the first event to fire, so in it, we set My.gblnUnloading.
My.OnReset() is triggered as objects collapse but skips reinitialization because gblnUnloading = True.

So, with this, we have created:
• An application events framework that permits any behavior trap to trigger an application event.
• A reset trap.
• In a standard module:
• A self-healing application event source property that any form, report, or class can use to raise or sink application events.
• A public variable to contain the default application event sink.
• Ultimately, a durable, lightweight, self-healing, and autostarting runtime environment.

The code in toto is attached.

- Eric Blomquist

EDIT:
• Zipped modules now incorporate Debug.Print lines to demonstrate event progression.
• Zipped ACCDB incorporates zipped modules and is 32-bit A2007, so should run in all subsequent versions.
 

Attachments

Last edited:
I'm not sure I would personally benefit from this approach, for 2 reasons:
  1. My end users always run my applications in runtime mode, which means any unhandled error would immediately crash the application anyway; and
  2. I use vbWatchdog as my error handler of last resort, so I never have to worry about unhandled errors in the first place.
Where I might find something like this useful is during development, if there is some way to preserve application state when clicking the square VBA reset button. However, that is essentially a nuclear option. Clicking that button would immediately cease all VBA execution, including the form unload event in frmCanary, which kicks off the process to save application state (if I understand correctly). Am I missing an obvious use case?
 
I'm not sure I would personally benefit from this approach, for 2 reasons:
  1. My end users always run my applications in runtime mode, which means any unhandled error would immediately crash the application anyway; and
  2. I use vbWatchdog as my error handler of last resort, so I never have to worry about unhandled errors in the first place.
Where I might find something like this useful is during development, if there is some way to preserve application state when clicking the square VBA reset button. However, that is essentially a nuclear option. Clicking that button would immediately cease all VBA execution, including the form unload event in frmCanary, which kicks off the process to save application state (if I understand correctly). Am I missing an obvious use case?
Thanks for your thoughts.

You're correct that frm.Canary.Form_Unload() triggers the event. Pressing the square VBA reset button triggers the event overtly.

I also typically have universal error handling in an app, and certainly have it in my base library. I don't currently use vbWatchdog although that may simply be a matter of time. This said, one use case could be a generic way to address unhandled errors.

The use case, really, is up to the developer, however. To the extent of the runtime framework as presented, reset will not preserve application state, i.e., the runtime framework will self-heal. Beyond that, what happens is merely what the developer codes in the Application.Reset event procedure, wherever sunk.

What this demonstrates is several things, the possibility of an Application.Reset event procedure being only one of them. Application_Reset() may be useful or not. Otherwise, this demonstrates:
• An application runtime environment (frmMain and My) and an application event framework built on it. This benefits from Application.Reset because it is self-healing as presented but need not be. Having the event framework up at all times can have utility, however.
• The application event framework is not limited to an Application.Reset event. The framework will support any event one might imagine. Any procedure in any module can trap behavior and then can raise any application event by calling, e.g., My.AppEventSource.OnEvent.
• In addition to the default event sink, any form, report, or class also can sink any application event by declaring WithEvents a clsAppEventSource variable and assigning My.AppEventSource to it.
• Coincidentally, that properties and variables in a standard module can have utility for event handling.

The specific use case that I had in mind when I got started with this was subscribing to the window message queue. Then, a WndProc callback can trap a particular window message, e.g., ActivateApp, and raise a corresponding application event, e.g., to restore focus to the last-opened a PopUp form that otherwise wouldn't get it when tabbing back into Access from another application. Other use cases might include anything in Win32 that one might declare, or .NET that one might wrap for COM. So, e.g., socket activation might come in handy.

I hope that answers some of your questions.
 
  1. My end users always run my applications in runtime mode, which means any unhandled error would immediately crash the application anyway; and

The above is not quite true!

If you use a accDB (non complied) application then YES the above is true.

However, if you use a compiled accDE, then the above is NOT the case.

In fact, any un-handled error will NEVER re-set local or global vars.

Hence, we always deploy a accDE.

Thus, even application that don't have error handling everywhere (which is not worth to do so IMHO anyway), the 2nd added bonus is that such errors don't stop the runtime EVER.

Needless to say, this then gives you 100% freedom as a developer to use some global vars, or even some global class with variables, and they will NEVER re-set - NEVER!

This gives your application the reliability of a un-stoppable freight train.......

So, by adopting a accDE, then all worries about some un-handled error goes away.

And as noted, the 2nd bonus is that the runtime ALSO never stops, or shuts down due to such un-handled errors.

The end result?
You actually don't have to worry about some re-set, since it will NEVER occur, and your VBA variables will NEVER under any case I am aware of go out of scope...


R
Albert
 
Just as an additional note:
It is not enough to rename an accdB to accdE. It must definitely be a database compiled to accdE.
The accdE can also be renamed to accdR.
 
@riktek
I've tried your example database and have studied the execution flow sequence.
Although your explanation of the sequence of events was very clear, having all the Debug.Print statements was very useful for me to understand what was going on.

You've obviously put a lot of work into this solution and I don't want to appear negative in any way.
However, whilst it appears to work exactly as described, I'm also unsure about how useful it is to me in practice.

I try to handle all possible errors in ACCDB files during development so only rarely do I find variables go out of scope due to a crash.
Where that does happen, it is useful as an incentive to fix the inherent problem
Otherwise, using TempVars instead of global variables normally solves that problem anyway.

In addition, my FE files are normally distributed as ACCDE files anyway.

Perhaps you could create a sample database demonstrating a typical problem that would be solved using this approach.
 
You've obviously put a lot of work into this solution and I don't want to appear negative in any way.
@isladogs, you're quite kind and thanks for responding, but I actually would invite negativity, most of all from you. None of us have any hope of learning anything otherwise and my ego will be quite unaffected.

However, whilst it appears to work exactly as described, I'm also unsure about how useful it is to me in practice.
Two points on this, one related to the event framework and another regarding the Application_Reset() event procedure (oAppEventSource_Reset() literally, as presented):

The event framework, including the runtime environment that supports it, has general utility insofar as one might be concerned with application events. I.e., it provides a framework within which one can (a) declare and raise any application event one wishes; (b) trigger that event from any procedure in the application; and (c) sink that event in any form, report, or class in the application. A default application event sink exists for general responses and other objects can sink and handle application events discretely. In a simple case, this can be used for inter-object communication within the Access library, or in more complex cases, one can trap behavior exposed in Win32 procedures one might declare or in .NET classes wrapped for COM. Sinking such event in a form superclass would permit all open forms to respond consistently.

The specific use case that inspired this, as I mentioned above in my response to @NoLongerSet , is to detect when the Access application becomes active, i.e., when alt-tabbing back into Access from another application. I had been frustrated by focus not returning to the last-opened form when it is a pop-up. I have a routine to do that while Access is active but it doesn't trigger when returning from another application. My solution is to subscribe to the window message queue and have a WNDPROC callback raise a custom ActivateApp event within this framework whenever WM_ACTIVATEAPP came across the wire. I'll endeavor (endeavour?) to post a demonstration presently (it has several components, so stay tuned) but this is but a single example of a procedure in a remote corner of an application raising an application event that any object can handle.

The Application_Reset() event procedure is simply another such example. To be clear, a project reset will occur on an unhandled error. Many commenters thus have associated this solution with error handling. That is a mistake, however, because we can't put that genie back in the bottle. I.e., we can't handle an otherwise unhandled error after a reset, or somehow reverse the reset. I also implement universal error handling and this solution isn't a substitute. The utility of Application_Reset() in fact has nothing to do with error handling per se but instead provides a way to respond programmatically to the consequences of a reset once it occurs.

The chief use case for Application_Reset() in my view (others doubtless will be more creative) is to restore an application's responsiveness, i.e., to re-instance event sinks application-wide. As presented, the application framework with My.OnReset(), inter alia, self heals, i.e., it re-instances the application event source and the application's default event sink. So, to this extent, it is somewhat self-preserving and self-referential. Most (all, actually) applications I develop also implement a lightweight framework of object-specific superclasses that configure the appearance and behavior of all runtime UI objects, including forms and controls, consistently across the application. Without more, a project reset will collapse these frameworks, taking down their event handling and destroying this functionality. To work at all, forms will have to be reopened, with the hidden application form being a special case. Now, with this solution, on a project reset I can iterate Forms, re-bind each to a fresh form superclass instance, and thus restore function application-wide.

Put otherwise, a project reset is a misnomer. Really, it's more a project un-set, if not upset. This solution provides a mechanism for an actual project reset to occur.
 
I've always worked on the FULL version of Murphy's Law.

1st half: Whatever can go wrong WILL go wrong.
The usually forgotten 2nd half: Therefore, make it so that it cannot go wrong.

Computer and other socket designs that can only be plugged in one way are a side-effect of the 2nd half. Polarized plugs, odd-shape 3-wire connectors, a pentagonal power cord for PCs, ... all of those are simply designs based on Murphy's Law.

The VBA version of this is that you test thoroughly before releasing your beast to the public. Then the only problems you will have will be external to Access or your app, and therefore making the app "persistent after an error" is sort of overkill.
 
The VBA version of this is that you test thoroughly before releasing your beast to the public. Then the only problems you will have will be external to Access or your app, and therefore making the app "persistent after an error" is sort of overkill.
All true, and thanks.

Canary > My.OnReset isn't at all about error handling, however. It's about moving event handling into a standard module.
 
Otherwise, using TempVars instead of global variables normally solves that problem anyway.
True but TempVars cannot contain objects.

Canary > My.OnReset is concerned with runtime state, not data or even error handling. Jailbreaking event processing and moving it into a standard module is the mechanism.
 
Last edited:
Canary > My.OnReset isn't at all about error handling, however. It's about moving event handling into a standard module.

There are arguments regarding error handling that suggest that while logging and message presentation could be handled in a sub, true error handling should ALWAYS be trapped and handled in the module that raised the error. What you call from an error handler is up to you. But WHERE you handle the error generally should be where it happens because of scope/locality issues.
 
There are arguments regarding error handling that suggest that while logging and message presentation could be handled in a sub, true error handling should ALWAYS be trapped and handled in the module that raised the error. What you call from an error handler is up to you. But WHERE you handle the error generally should be where it happens because of scope/locality issues.
That's actually quite interesting and thanks. I hadn't thought about module-level error handling. I'll need to noodle a bit on what that might look like.

You're also entirely correct that what one calls from an error handler is up to the developer.

I also mis-spoke a bit. I said it's about moving error handling into a standard module but that misses the point somewhat, in two ways: Probably due to insufficient caffeination, I wrote error handling, not event handling, and it isn't a generic event handler.

More precisely, two things: First off, It's about declaring and raising application-level (or perhaps project-level) events. Procedure- and module-level error handling are entirely beside the point. Many things might trigger an application-level event. I've cited the use case of a WNDPROC callback calling an OnActivateApp hook as one example. The Application.Reset event this technique describes is simply another such application-level event. Yes, of course, it will be triggered by a failure of procedure- or module-level error handling. That's obvious, just as it's obvious that a user Alt + Tabbing into an app will trigger a WM_ACTIVATEAPP window message. Every application-level event will have a trigger of some sort. That doesn't mean that the application-level event procedure can address the trigger. An Application.Reset event procedure can't reverse or undo a project reset any more than an Application.ActivateApp event procedure triggered by a WM_ACTIVATEAPP window message can reverse or filter that message. It certainly can't do anything about the failure of error handling that caused the reset, any more than an Application.ActivateApp can influence the user's Alt + Tab input. This is why Canary > My.OnReset() has nothing to do with error handling. It's simply a way of raising an application-level event that heretofore has been programmatically unaddressable. Then, as you state, what one calls is up to the developer.

Second, it isn't actually about moving event handling into a standard module, as I first wrote. It's about moving event processing into a standard module. It's a partial jailbreak of sorts, in that an event source ordinarily must be an object, meaning an instance of a class, form, or report. It can't be a standard module. Similarly with event sinks: One can't declare an object variable WithEvents in a standard module. What this does, however, is two things:

• It defines the application event source as a property of a standard module. There can be only one application event source; a second instance will be identical but have no inkling of events that the first instance raises. This lone object also must have global visibility: It is an application event source, after all. This points to the application module, which, being a standard module, necessarily is a singleton. But again, being a standard module, it can't directly declare or raise events. It can have an object property, however, and that object can. So, the first partial jailbreak is this: The limitations of a standard module and its variables do not extend to the objects they contain. The property's backing variable has no visibility of its contained object's events. Nevertheless, any class, form, or report can sink those same events by assigning that same pointer to their own event source variable declared WithEvents.

• It contains the default application event sink in a standard module variable. The application module, being a standard module, can't sink events directly and can't declare a variable WithEvents. A standard module can have a variable pointing to an event sink, however. The second partial jailbreak is, essentially, jacking up the WithEvents variable onto another object variable, i.e., putting it in an object the latter contains.

So, we haven't really broken the rules about sources and sinks needing to be class instances, but by moving those elements into a standard module, we have created a robust means to declare and raise application events. Canary > My.OnReset is just a flourish given an application event framework, a sample application event previously undefined.

EDIT: Actually, a third bit of jailbreaking this does place an event hook in a standard module. My.OnReset() of course chains to My.AppEventSource.OnReset(), so no real rule breaking, but still.
 
Last edited:
I hadn't thought about module-level error handling.

Another factor: You have limited or no control over the way errors are handled because it isn't done by your code - it is done by Access using Windows facilities. If you take an error break in a module, the current stack frame is examined for a qualified (i.e. not currently active) error handler declaration. But here is the catch: Because of the mechanisms used, your ON ERROR GOTO label must point to a label that is local to that sub or function. (Which is part of the reason I said you had to handle errors locally to the routine that declared the handler.)

The side effect of not having a local error handler is that an error will collapse the stack frame for the current routine and look to its caller for a valid error handler declaration. If you had some sort of defensive code in the faulting routine, an error might not execute that code unless there IS an error handler present that can trigger said defensive code.
 
Another factor: You have limited or no control over the way errors are handled because it isn't done by your code - it is done by Access using Windows facilities. If you take an error break in a module, the current stack frame is examined for a qualified (i.e. not currently active) error handler declaration. But here is the catch: Because of the mechanisms used, your ON ERROR GOTO label must point to a label that is local to that sub or function. (Which is part of the reason I said you had to handle errors locally to the routine that declared the handler.)

The side effect of not having a local error handler is that an error will collapse the stack frame for the current routine and look to its caller for a valid error handler declaration. If you had some sort of defensive code in the faulting routine, an error might not execute that code unless there IS an error handler present that can trigger said defensive code.
You're right, of course, on all counts. Errors bubble up the call stack. All that's a given. Note the rest of the post to which you responded, which describes why Canary > My.OnReset is not concerned with error handling.

I have been implementing global local error handling for years. This is no substitute, as consistently noted.
 

Users who are viewing this thread

Back
Top Bottom