
FoxTalk 2.0 April 2006
Global Access and Assign Events in Visual FoxPro
Bogdan Zamfir
Starting with version 6.0, Visual FoxPro introduced _ACCESS and _ASSIGN methods. They added great flexibility for programmers. The classes can now validate the values of the properties when they're assigned, and custom code can be run when a property is read or changed. Business objects and visual classes can be created to be used in a much more "natural" way. THIS_ACCESS is a special case of the _ACCESS method, which is bound to the object itself, and gets called when any property or method of the class is accessed. Using it, you may try to implement a global handler to be called when any property is read. For example, you may try to implement a class to store application global settings or user preferences. It can use an INI file to store values of global application settings, and when an instance of the program needs the value of a setting, the global handler will read it from file. You can expect that, similarly, you can create a THIS_ASSIGN method, which will be called anytime when you assign a value to a property, allowing you to implement a generic handler when any property of the class is changed (that is, as soon as an instance of the program changes a setting, it should be saved to disk, so when another instance needs that setting, it will get the new, most up-to-date value). Unfortunately, this isn't the case. Even if you create the THIS_ASSIGN method, it doesn't work as expected.
After some research, and with the help of the BindEvent and RaiseEvent functions, I was able to implement a pair of custom global class events, with functionality similar to _ACCESS and _ASSIGN, which I describe here.
A little background about ACCESS and ASSIGN methods
Starting with version 6.0, when you create a new property for a class, you have the option to create ACCESS and ASSIGN methods for that property. For example, if you add a new property called CustomerID and choose to create ACCESS and ASSIGN methods, in the property window you'll see two new methods created automatically, customerid_assign and customerid_access. If you look at the body of customerid_access, you'll see the following code added automatically by FoxPro:
*To do: Modify this routine for the Access method
RETURN THIS.customerid
And you'll see the following code in the body of customerid_assign:
LPARAMETERS vNewVal
*To do: Modify this routine for the Assign method
THIS.customerid = m.vNewVal
As you can see, the default methods simply return or set the value of the corresponding property. But they open a whole world to you as a programmer. You can write your own code in both ACCESS and ASSIGN methods. For example, if the CustomerID property belongs to a class implementing a business object, in the ASSIGN method you can implement the code to load all customer data from the database for a certain CustomerID, or raise an error if the user attempts to assign an invalid Customer ID. Here's a possible new version for customerid_assign:
LPARAMETERS nCustID
Local lFound
*-- ensure nCustID is number
nCustID = iif(vartype(nCustID)="N", nCustID, 0)
*-- search for customer ID
lFound = indexseek(nCustID, .f., nCustID, ;
"customer_id")
if lFound
*-- find the actual record
indexseek(nCustID, .t., nCustID, "customer_id")
this.CustomerID = nCustID
*-- set the rest of the properties
this.CustomerName = CUSTOMERS.CustName
this.CustomerAddress = CUSTOMERS.CustAddress
*--
Else
*-- if customer id wasn't found, error
this.CustomerID = 0
error "Invalid customer id"
endif
Similarly, the ACCESS method can perform some processing before returning a result. Considering the same Customer class, we can have a property DiscountPercent. Its value depends on the total amount of orders from the previous month, as shown here:
*-- DiscountPercent_access method
If this.CustomerID=0 then
Error "Invalid customer"
Else
Select sum(order_amount) as TotalAmount ;
From Orders ;
Where month(order_date)=month(date())-1 ;
And order_custid=this.CustomerID ;
Into cursor tmpTotalAmt
Return icase( ;
tmpTotalAmt.TotalAmount>10000, 15, ;
tmpTotalAmt.TotalAmount>1000, 10, ;
tmpTotalAmt.TotalAmount>100, 5, ;
0)
Endif
For more information regarding the ACCESS and ASSIGN methods, check the MSDN library (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_foxhelp9/html/0f0717bb-ec5c-4d09-a80d-0afb19466c4b.asp).
A special case is the THIS_ACCESS method. It works as a global ACCESS method and is called when any property or method of the class is used (actually before the property is used or the method called). To create it, you have to add it as any regular method (select the menu Class | New Method, type THIS_ACCESS in the Name box, and click the Add button). If you look at the code created by default by FoxPro, you'll notice it's different from the default code created for an ASSIGN method for a regular property:
LPARAMETERS cMember
*To do: Modify this routine for the Access method
RETURN THIS
As you can see, unlike the ACCESS method for a property, the THIS_ACCESS method receives a parameter. That parameter is the name of the property or method that was used, which caused THIS_ACCESS to be triggered. This presents a large number of possibilities. For example, using the same Customer business class mentioned earlier, we can have several properties, related to fields in a Customer table, which can change very often. We want to always return the actual value from the database. Instead of writing code in every _ACCESS method for all of those properties, we can write a global handler. If the accessed property is in the list of properties we need to return the most up-to-date values for, we can read it from the database.
LPARAMETERS cMember
*To do: Modify this routine for the Access method
Local lFound
If cMember $ this.cPropertiesLive
lFound = indexseek(this.CustomerID, .t., ;
"customers", "customer_id")
If lFound
do case
case cMember = "nlastorder"
this.nLastOrder = customers.last_ord
case cMember = "ccontactname"
this.cContactName = ;
customers.contact_name
endcase
endif
Endif
RETURN THIS
Seeing the beauty of THIS_ACCESS, you might think you also have THIS_ASSIGN, which should be called when you set any property of the class. Wrong!
If you try to add a THIS_ASSIGN method to a class, you'll notice FoxPro creates it and even sets some code in it by default.
LPARAMETERS vNewVal
*To do: Modify this routine for the Assign method
THIS.THIS = m.vNewVal
However, this code is wrong, and trying to run it should generate a runtime error because the code generated by FoxPro tries to return the value of a property named THIS, which doesn't exist. But the worst part is that this code isn't even called automatically by FoxPro when you assign values to any property of the class. Actually, you'll notice that when you assign any value to any property, FoxPro still calls the THIS_ACCESS method!
Giving it some deep thought, this behavior actually seems correct. When you assign a value to a property, you actually access the object itself, to be able to get a reference to the property. So accessing the object results in calling THIS_ACCESS, and passing it the name of the property you're about to change.
However, having a global handler similar to THIS_ACCESS, but which is called when you assign values to any property, would be a very nice feature. Suppose you want to implement a class that stores global options for an application. Such a class can have tens and even hundreds of properties, for a mid-sized application. As soon as a user changes a global setting, you need to save it on the disk, so other instances of the program, running on other computers, can read it right away and update their behavior accordingly. Also, it's very likely that with every new version of the program you release, you might need to add new global settings (so new properties in the application global settings object). However, for tens or hundreds of properties that store and read those settings, writing code in their corresponding _ASSIGN methods will soon became a maintenance nightmare.
Given that situation, I'm not prepared to give up so easily on a THIS_ASSIGN global handler method.
As you can see in Figure 1, when a property of the class is changed, the THIS_ACCESS method is called first, if it was defined for the class. Then the assignment is executed and the normal execution flow continues. If for a changed property you've defined an _ASSIGN method, it's called in order to perform the assignment, and the actual assignment causes the THIS_ACCESS event to be called again.
Figure 1

Similarly, when a property is read, THIS_ACCESS is called first, and then the property is read. If you defined an _ACCESS method for the property, it will be called, and when the code in _ACCESS actually reads the value from the property, THIS_ACCESS will be triggered again. To show this behavior, in the download file I created a small class bzTestAccessAssign in module bz_TestAccessAssign. It has a custom property, TEST, with TEST_ACCESS and TEST_ASSIGN methods, and a global THIS_ACCESS method. You can run the code shown in Figure 2 and you'll get the same result.
Figure 2

This proves that every time a property is accessed (to read or write, even inside of an _ACCESS or _ASSIGN handler), THIS_ACCESS is called again. This behavior is caused by the fact that, in the property's _ACCESS or _ASSIGN handler, when the property is actually read or written, the code uses the THIS object, which triggers THIS_ACCESS again. This can cause unpredictable (and undesired) results, because if I change the property once in the global handler, then again in the property's own _ACCESS method, THIS_ACCESS is called after its own _ACCESS, and it can change the value of the property. The implementation described below will take care of this problem, by preventing it (so the global handler will be called only once, and then, eventually, only the property's own _ACCESS method will be called, to perform any additional processing).