Resource files (RES files) are good things in which to keep your error text and messages, and most C developers use them all the time, especially if they're shipping products internationally. That said, Visual Basic itself uses resource files-recognize some of these sample strings taken from Visual Basic's own resources?
In fact, Visual Basic 6 uses a total of 2,934 resource files.
The %s in string 13031 is used to indicate (to a standard C library function) wh 525n1322f ere a substring should be inserted-the binary name (?.EXE, ?.DLL, ?.OCX) in this case. The |1, |2, and |3 in string 23284 shows where replacement strings should be inserted, this time using a different technique. In fact, this latter technique (which you can use even on the %s strings) can be seen operating if you look at ResolveResString in the Visual Basic source code for SETUP1.VBP. It looks more or less like this (this is slightly tidied up):
To see all this code work, compile the strings and add them to your project. (Save the strings as an RC file, and then run the resource compiler on the RC file like so: C:\rc -r ?.rc. Add the resulting RES file to your application by selecting Add File from the Project menu.) Then add this code to Form1's Load event:
This will produce the following message box:
Keeping message text in a resource file keeps strings (which could include SQL strings) neatly together in one place, and also flags them as discardable data, stuff that Windows can throw away if it must. Don't worry about this-Windows can reload strings from your binary image if it needs them. Your code is treated in exactly the same way and you've never worried about that being discarded, have you? Keeping read-only data together like this allows Visual Basic and Windows to better optimize how they use memory. Resource files also provide something akin to reuse as they usually allow you to be "cleverer" in your building of SQL and error text-they might even provide a way for you to share data like this across several applications and components. (See the notes on using a ROOS earlier in this chapter.)
When a control raises an unhandled error (by the control), the error is reported and the control becomes disabled-it actually appears hatched-or the application terminates. (See Figure 1-7 and Figure 1-8.)
Figure 1-7 Containing form before the error
Figure 1-8 Containing form after the error
It's important to know that errors in a UserControl can be propagated to two different levels. If the errors are caused wholly by the control, they will be handled by the control only. If the errors are instigated via a call to an external interface on the control, from the containing application, they will be handled by the container. Another way to state this is to say that whatever is at the top of the call stack will handle unhandled errors. If you call into a control, say from a menu selection in the container, the first entry in your call stack will be the container's code. That's where the mnuWhatever_Click occurred. If the control raises an error now, the call stack is searched for a handler, all the way to the top. In this case, any unhandled control error has to be handled in the container, and if you don't handle it there, you're dead when the container stops and, ergo, so does the control. However, if the control has its own UI or maybe a button, your top-level event could be a Whatever_Click generated on the control itself. The top of your call stack is now your control code and any unhandled errors cause only the control to die. The container survives, albeit with a weird-looking control on it. (See Figure 1-8.)
This means that you must fragment your error handling across containers and controls, not an optimal option. Or you need some way of raising the error on the container even if the container's code isn't on the stack at the moment the error occurs. A sort of Container.Err.Raise thing is required.
In each of our container applications (those applications that contain UserControls), we have a class called ControlErrors (usually one instance only). This class has a bundle of miscellaneous code in it that I won't cover here, and a method that looks something like this:
Public Sub Raise(ParamArray v() As Variant)In each container application we declare a new instance of ControlErrors, and for each of our UserControls we do what's shown below.
UsesControlErrors returns True if the UserControl has been written to "know" about a ControlErrors object.
In each control-to complete the picture-we have something like this (UsesControlErrors is not shown):
Private ContainerControlErrors As ObjectWe know from this context that SomeUIWidget_Click is a top-level event handler (so we must handle errors here), and we can make a choice as to whether we handle the error locally or pass it on up the call chain. Of course, we can't issue a Resume Next from the container once we've handled the (reporting of the) error-that's normal Visual Basic. But we do at least have a mechanism whereby we can report errors to container code, perhaps signalling that we (the control) are about to perform a Resume Next or whatever.
Raising errors in a Visual Basic OLE Automation server is much the same as for a stand-alone application. However, some consideration must be given to the fact that your server may not be running in an environment in which errors will be visible to the user. For example, it may be running as a service on a remote machine. In these cases, consider these two points:
Don't display any error messages. If the component is running on a remote machine, or as a service with no user logged on, the user will not see the error message. This will cause the client application to lock up because the error cannot be acknowledged.
Trap every error in every procedure. If Visual Basic's default error handler were executed in a remote server, and assuming you could acknowledge the resulting message box, the result would be the death of your object. This would cause an Automation error to be generated in the client on the line where the object's method or property was invoked. Because the object has now died, you will have a reference in your client to a nonexistent object.
To handle errors in server components, first trap and log the error at the source. In each procedure, ensure that you have an Err.Raise to guarantee that the error is passed back up the call stack. When the error is raised within the top-level procedure, the error will propagate to the client. This will leave your object in a tidy state; indeed, you may continue to use the same object.
If you are raising a user-defined error within your component you should add the constant vbObjectError (&H80040000&). Using vbObjectError causes the error to be reported as an Automation error. To extract the user-defined error number, subtract vbObjectError from Err.Number. Do not use vbObjectError with Visual Basic-defined errors; otherwise, an "Invalid procedure call" error will be generated.
|