Thursday, February 23, 2012, 11:04:00 PM
Here's a question that comes up quite frequently about Web Connection: How do I create and manage global variables effectively in a Web Connection application?
Web Connection is somewhat unique in terms of Web back end solutions in that it actually has a dedicated server component and so has some 'state' - server state - that persists between individual requests. There are a number of ways that you can store 'global' data in a Web Connection instance:
- PUBLIC variables
- Properties on the wwServer instance
- The wwServer::oGlobals Property
- The wwServer::oResources Dictionary
- The wwServer::oCache Dictionary
But before we delve into the details of each of these mechanism it's important to understand what 'global' means in the context of a Web Connection application.
Global scope in Web Connection
Every Web Connection application starts out with one or more server instances that stick around as long as the instance is running. An instance is always tied to a specific EXE or DLL: The FoxPro IDE in development mode, a standalone FoxPro EXE when running in deployed file mode, or as COM EXE or DLL in COM mode.
In file mode this means the instance is running sitting on a READ EVENTS loop, in COM mode it means the COM instance instantiated by the client IS the actual server instance. In COM mode (both ISAPI and .NET and EXE or DLL modes) Web Connection runs on a maintained pool of instances that are managed so these instances actually stay alive as long as the server instance is live.
The important point here is that each EXE or DLL loaded instance it's its own silo of data that is separate from another instance. So while you can create global data using any of the mechanisms used in the list above, they are global only within the context of the current instance. Each Web Connection server has its own instance of global data - there's no in memory sharing between instances. The only way you can share data between instances is to store data to a database, disk or some other inter-process communication mechanism.
That said, it's still quite useful to have global data in an application. Global data is good for resources that are expensive to load up repeatedly. For instance SQL Server connections are slow to connect each time, but very fast once open and already connected so it makes sense to store a SQL connection globally so it can be used across multiple requests. Other objects like COM components or any sort of cached memory based data also qualifies.
How Web Connection Instances are loaded
Web Connection instances are tied directly to a wwServer instance. In File Mode an instance of your wwServer subclass is created in the startup code in YourAppMain.prg which looks like this:
*** Load the Web Connection class libraries
*** Load the server - wc3DemoServer class below
goWCServer = CREATE("wcDemoServer")
SET DEBUG OFF
SET STATUS BAR OFF
SET DEVELOP OFF
SET RESOURCE OFF
SET SYSMENU OFF
=MessageBox("Unable to load Web Connection Server",;
MB_ICONEXCLAMATION,"Web Connection Error")
*** Make the server live - Show puts the server online and in polling mode
The instance is created which loads the server instance and the server form (if enabled) and then pops into a READ EVENTS which keeps the server alive.
In COM Mode the startup code goes away and the server instance is directly created via COM:
loServer = CREATEOBJECT("wcDemo.wcDemoServer")
loServer2 = CREATEOBJECT("wcDemo.wcDemoServer")
The server can be started as an EXE or a DLL, but the behavior is the same. If I create multiple instances like above I can set properties on each and they are treated independently. Any PUBLIC data in each of these servers too are completely independent of each other.
Don't treat Global data like you do in Desktop Application
A common misconception for newbies to Web development is that you can use PUBLIC variables or even global cursor to persist data on a per user basis. Unlike desktop applications, which are typically tied to a specific user, Web applications serve requests for many users. Each Web Connection instance isn't tied to a specific user so anytime a request comes in it can come from any user that's using the system. If multiple instances are running the same user may not even come back the same instance in two consecutive requests.
In short, global data for user data storage - both memory or using temporary cursors - in Web applications is a terrible idea in most cases. The exception is if you decide on some useful naming scheme to do but even then persistence across requests should be avoided whenever possible.
Global scope then is only really useful for storing things that truly are application wide and preferably should only be used for things that really require it. Typically this means slow to load operations (like SQL connection loading for example) or pre-loading of data structures that can then be more easily reused later.
Storing Global Data in Web Connection
Let's look at the different approaches available from least desirable to more desirable.
PUBLIC: Avoid whenever possible!
In desktop applications PUBLIC variables are an easy (if frowned upon) mechanism for storing global data and this mechanism still works in Web Connection. However, due to the possibility of scope naming conflicts and the inability to effectively manage PUBLIC variables it's not a good idea to use them.
That said, some of Web Connection's internals actually use PUBLIC vars. Some library functions create global cached instances of objects that can be reused using PUBLIC. These objects use PUBLIC because they are generic and can be used outside of Web Connection (for example GetwwDotNetBridge() or GetwwRegEx()). Each of these functions create PUBLIC instances with __ prefixes to minimize naming conflicts and are used to cache resources that are expensive to create.
In general application development (outside of libraries) I otherwise can't see any good reason to use PUBLIC variables.
Properties on the Web Connection wwServer Instance
A Web Connection server instance is defined by its wwServer subclass implementation. This is the server class and the class that persists through the lifetime of the server instance. It has state and remains active through potentially many requests until it is shut down through the Web Connection administration interface or by IIS when you restart the Web Server or otherwise recycle an Application Pool.
A server class definition looks like this:
**** YOUR SERVER CLASS DEFINITION ***
DEFINE CLASS wcDemoServer AS WWC_SERVER OLEPUBLIC
oMyProperty = .NULL.
and you can create any number of custom properties on your server instance.
The server is always available either via a global variable called goWCServer, or via the Process.oServer or simple Server variables inside of actual request processing in a wwProcess request or Web Control Framework page . If you create a custom property on wwServer and you want to access it in a process class or Web Control page you can always use:
Using wwServer instance properties works and reasonably clean, but if you are running in COM mode, anytime you add a property to your class the ClassIds change for your COM server. This means you have to re-register your COM object on the server, which is a bit of a pain.
In order to avoid the COM re-registering issue you can use the wwServer.oGlobal property and dynamically add properties at runtime. This object is merely an instance of a CUSTOM class attached to wwServer for the sole purpose of allowing you to hang new objects off it. To use it in code you can simply do:
This adds a property to the oGlobal instance which you can then use anywhere in your application:
There's nothing magical about this of course - it uses only VFP's native dynamic language features of runtime type extension. It does however sidestep the COM registration issue.
If you'd rather not use the .AddProperty() method at runtime and instead use a separate object that defines properties explicitly you can do that simply by assigning the oGlobal instance at runtime in OnInit() or OnLoad() of the server event.
For example, here I create a custom MyGlobalsContainerClass instance and assign it to wwServer.oGlobal and then use the custom class' configuration property later in code:
this.oGlobal = CREATEOBJECT("MyGlobalsContainerClass")
this.oGlobal.Configuration = CREATEOBJECT("StoreConfig")
Note wwServer.oGlobal was introduced in Web Connection 5.62. In version prior you can simply add an .oGlobals property to your server class:
DEFINE CLASS wcDemoServer AS WWC_SERVER OLEPUBLIC
oGlobal = null
this.oGlobal = CREATEOBJECT("Custom")
which is pretty much all that Web Connection does internally in the later versions.
Note that you can also add properties directly to the server object. wwServer derives from RELATION so it doesn't have an .AddProperty() method but you can use the ADDPROPERTY() function:
Although this works just as well as the previous example maybe with slightly simpler syntax for the resulting object, I prefer using the oGlobal object instead as it is clearer what's happening. Both dynamic adds avoid COM object re-registration, though, so the choice is purely a preference.
wwServer also has an wwServer.oResources dictionary of type wwNameValueCollection. Basically this is a memory based dictionary that can hold any kind of variable including objects. So rather than actually adding properties to a class you can add and access these global values using a simple dictionary type syntax.
To use the resource entry you can then this from within Web Connection code.
loMyObject = Server.oResources.Item("MyProperty")
The retrieval and usage syntax requires an intermediary since FoxPro doesn't allow for executing methods of return values directly, but otherwise the behavior is very similar to the approach using custom properties. The thing that's nice about the dictionary is that your object structure doesn't change at all and you can easily iterate over all items in the resource dictionary.
wwServer.oCache Global Caching
On a somewhat related but not directly relevant note there's also the wwServer::oCache object. This wwCache object allows storing string values for a specified expiration time. It stores data in a cursor or - optionally - in a fixed table that can be accessed by multiple instances. wwCache is limited to string values. Although the class is meant for caching you can effectively hijack it as a long term object store by setting a very long timeout for the cache expiry. If you use a fixed filename the cache persists to a table that can in fact work across instances and can survive an instance shutdown and startup.
To add an item to the cache from the server class:
and to read it back in a process class:
Cache is interesting, but it's somewhat limited in that it works only with strings unlike all the previous mechanisms which can store values and objects. Still it might be useful for some scenarios that require global data/state to be persisted and it is the only option that allows multiple instances to share data from within the framework.
There are a number of ways to store global variables in Web Connection which is somewhat unique for Web based environments. But just because you can doesn't mean that you should. Always be very clear about why you need to store data or objects globally and think long and hard about it. Global data consumes resources on the server and it's resources that always stay loaded. If you really need those resources frequently then it makes sense to have it stored globally. But if these resources are rarely used or they are not expensive to load up in the first place it might be easier and more maintainable to simply create them as needed.
There's little reason to use PUBLIC variables in Web Connection applications. Take advantage of the provided global stores from object properties hanging off the wwServer instance to the wwServer.oResources dictionary or if you're dealing with strings possibly the wwServer.oCache object. Lots of options abound.