My users can't see this stuff, but I can, and I love it
As usually happens with every new version of Visual FoxPro, we have plenty of new "non-visual" enhancements. These enhancements are not noticed by our end-users, but we really enjoy them: new classes, commands, functions, and any thing else that can make our life as developer easier, more productive, and that can give us several new ideas to work with.
In this article, I'll show you this type of new features in VFP 8 Beta. Of course, it's impossible to show you all of them on a single article. So I chose those I loved the most, and that I think you'll enjoy too. Let's go (are you ready to go?! )!
The New CursorAdapter class
The CursorAdapter new class gives us new opportunities to extend our data cursors, it helps us to work with local and remote data, from Native VFP data to ODBC, ADO and XML.
Its use is pretty straight-forward. Take a look at the next sample. We start by defining a new class, based on the CursorAdapter baseclass:
Define CLASS CustomersData as CursorAdapter *-- Let´s define the DataSourceType as "Native" (VFP data) DataSourceType="Native" *-- Let´s define the name of the alias that should be created. Alias="curCustomers" *-- Let´s define the command that will generate the data to fill the cursor. SelectCmd = "select * from Customer" Procedure Init *-- Once the object is created, let´s fill the cursor. this.CursorFill() EndProc EndDefine
The CursorFill() method is part of the PEMs of this new baseclass. Everytime it´s called, it will use the SelectCmd property to get the data and fill the cursor. We could use the class above like this:
oCustomersData = CreateObject("CustomersData") Browse
Remember: once the object is created, there will be a curCustomers cursor opened at the browse.
Do you want a little more fun? Well, let´s take a look at this sample:
Define CLASS CustomersData as CursorAdapter *-- Now, we want to define the type of the data source as ODBC DataSourceType="ODBC" Alias="curCustomers"
*-- We can define a "Cursor Schema" CursorSchema = ; "customerid c(5),companyname c(30),contactname c(30),"+; " contacttitle c(30), address c(30), city c(30), country"+; " c(30)" SelectCmd = ; "select customerid, companyname, contactname, "+; " contacttitle, address, city, country from Customers" *-- Remember the CursorSetProp() function? :) Tables = "Customers" KeyFieldList = "customerid" UpdatableFieldList = "companyname" UpdateNameList = "companyname customers.companyname" Procedure Init *-- Add a property at run-time that holds a Connection Handle. This.AddProperty('Connection',; SQLSTRINGCONNECT(; "Driver=SQL Server;Server=(local);"+; "Description=Northwind, Int Security;"+; "DATABASE=Northwind;uid=sa;pwd=;")) *-- The data source is our Connection Handle This.DataSource = This.Connection This.CursorFill() EndProc EndDefine
As I pointed out by a comment in the code above, we have some properties that will turn the cursor into an "updatable cursor", just as we are used to doing with the CursorSetProp() function.
Can you see the beauty of this? We have just created a class that, when we use it, it will bring us an updatable cursor filled with data. The "objectified" way to do this is really cool.
VFP ads advertise: "Can you feel the XML"? Take a look at this one:
Define CLASS WishListData as CursorAdapter *-- Time to set the source type to "XML"! DataSourceType="XML" Alias="curWishList" Procedure Init *-- Set the SelectCmd property to the method *-- that will return some XML data. This.SelectCmd = "this.GetXml()" *-- Fill the cursor. This.CursorFill() EndProc
Procedure GetXml *-- Access our beloved UT´s Web Service over ther Internet *-- and get some data, Sir! loUniversalThread=Createobject("mssoap.soapclient30") loUniversalThread.mssoapinit(; "http://www.universalthread.com/WebService/VisualFoxPro.wsdl") lcUsername='MyUser' lcPassword='MyPassword' loUniversalThread.Login(lcUsername,lcPassword) lcXML=loUniversalThread.GetWishList(DATE()-20) RETURN lcXML EndProc EndDefine
Just think about it: We told to the CursorAdapter that we´ll work with "XML". We set the SelectCmd property to a Method on the class that will return a XML string. On this method, we just get the XML from any place we want, and the Adapter will take care of converting the data to a VFP cursor. Isn´t that pretty cool?
Do you remember wishing for a better integration with ADO? Well, now we have it. Take a look at this:
Define CLASS CustomersData as xCursor Procedure Init *-- Our Source Type now is "ADO" This.DataSourceType = "ADO" *-- We´ll add a property to store an ADO Connection object. This.AddProperty('oConn',; NewObject("ADODB.Connection")) *-- And another property to store an ADO RecordSet object. This.AddProperty('oRS',; NewObject("ADODB.Recordset")) *-- We´ll set the Connection String for the Connection object, *-- as we´re quite used to. This.oConn.ConnectionString = ; "Provider=SQLOLEDB.1;"+; "Initial Catalog=Northwind; Data Source=lassalanotebook;uid=sa;pwd=;" *-- ...and open the connection. This.oConn.Open() *-- Let´s set this newly created connection as the Active Connection *-- for our RecordSet object. This.oRS.ActiveConnection = This.oConn *-- ...and set the RecordSet as the data source for this Cursor Adapter. This.DataSource = This.oRS
*-- This is our command to bring our data. This.SelectCmd = ; "select customerid, companyname, contactname, "+; "contacttitle, address, city, country from customers" *-- And fill up our Cursor with data, big boy! This.CursorFill() EndProc EndDefine
Can you see how easy it is? Just by doing this we have a VFP Cursor filled with data that comes from an ADO RecordSet.
Well, I think you still want more fun. So, let´s keep talking? We always dreamed of an easy way to bind ADO RecordSets to our forms, and also keep using the RAD features not just with native tables, but with local and remote views.
What about using the IntelliDrop (drag from the DataEnvironment into a Form in the Form Designer) to bind controls to data that comes from RecordSets, XML, any ODBC data source, or something like that? Well, now we have this ability.
Let´s suppose I have this form, with a simple grid on it. Now, what I want is to access some data on my SQL Server using ADO, and show this data on my native VFP grid. Well, by right-clicking on the DataEnvironment, we have a new option, called "Add CursorAdapter". Taking a look on the properties of this thing, you´ll see some nice properties, like "KeyFieldList", "UpdatableFieldList", "SendUpdates", and that sort of thing.
A cool property that we must use in order to better benefit from the IntelliDrop feature is the CursorSchema property. On this property we must input the "schema" of the cursor (aka, the structure of the cursor). By defining this schema, the IntelliDrop will work just like when we drag a field from a table and drop it into the Form.
I´ll name this control as "CursorAdapterCustomers", and set the Alias property as "Customers".
Finally, I´ll fill the Init method of the form with the following code:
*-- Let´s point out a reference to our CursorAdapter. With Thisform.DataEnvironment.CursorAdapterCustomers *-- The following lines you already know from some previous samples. .DataSourceType="ADO" .SelectCmd = ; "select customerid, companyname, contactname, "+; " contacttitle, address, city, country from Customers" .AddProperty('oConn',null) .AddProperty('oRS',null)
.oRS = CreateObject("ADODB.RecordSet") .oConn = CreateObject("ADODB.Connection") .oConn.Open("Driver=SQL Server;Server=lassalanotebook;"+; "Description=Northwind, Int Security;"+; "DATABASE=Northwind;uid=sa;pwd=;") .oRS.ActiveConnection = .oConn .Datasource = .oRS .CursorFill() Endwith *-- And now, we can define our cursor as the source for our grid. This.grdCustomers.RecordSource = "Customers"
When we run the form, we have our data on the grid.
The new AddProperty() and RemoveProperty() functions
Everybody who uses objects created with the SCATTER NAME command has always wished for some way to add properties on-the-fly to those objects. As a workaround, we ended up using either the AddProp5.fll founded on the web, or creating manually the object based on a "light" class (like Relation baseclass, for instance) and adding the properties based on the fields of the cursor.
We don't need to do that anymore: Now we have a new function called AddProperty, that we can use to add properties on-the-fly for "scattered" objects, or objects based on the new Empty Class (see more about that class in this same article), as those objects don't have a native AddProperty() method.
Take a look at the sample below:
*-- First, let's bring some data... Select Company_Name, Contact_Name From (_samples+"\tastrade\data\customer") ; into Cursor curCustomer *-- Then, let's created the "scattered" object. Scatter name loCustomer
*-- And finally, let's add a new property called MyNewProp to the object, *-- initializing it with the value "MyValue". AddProperty(loCustomer, "MyNewProp", "MyValue") *-- We can query our brand new property ? loCustomer.MyNewProp
Likewise, we can now remove properties on-the-fly, using a new function called RemoveProperty, just like this:
RemoveProperty(loCustomer, "Contact_Name")
The new Empty class
Until VFP 7, everytime we needed a "light-weight" class (a class that's fast to initiate and destroy), we ended up using the Relation baseclass; that's kind of strange, as the main purpose of that baseclass is establish a relation between two tables on a DataEnvironment. In VFP 8 we have the Empty class.
The Empty class is a class with no PEMs (Properties, Events or Methods) that can't be subclassed nor defined in a PRG or VCX file. This class will be used when we need to create objects (like "scattered" objects) and add properties to them at runtime, or when we need to store any sort of data of an "objectified" fashion at runtime.
For example: unfortunately, there isn't a localized version of VFP in Portuguese. So, it's useful for us to have in our apps an object which stores Portuguese stuff for Months and Days-of-Week. Let's see how we could accomplish that by using the Empty class:
*-- First I'll define a "Utils" class, *-- based on "Session" baseclass, for example... Define Class Utils As Session *-- Now the method that will "Bring" something useful for us. Procedure BringUtils *-- Let's create our "Empty" object. Local loFoo loFoo = Createobject("Empty") *-- Time to use the new AddProperty() function. *-- First we add a property to store the Months... AddProperty(loFoo, "Months(12)") *-- and second a property to store the days of the week. AddProperty(loFoo, "DaysOfWeek(7)") *-- Let's store the data. With loFoo *-- The Months in Portuguese .Months(1) = "Janeiro" .Months(2) = "Fevereiro" .Months(3) = "Março" .Months(4) = "Abril" .Months(5) = "Maio" .Months(6) = "Junho" .Months(7) = "Julho" .Months(8) = "Agosto" .Months(9) = "Setembro" .Months(10) = "Outubro"
.Months(11) = "Novembro" .Months(12) = "Dezembro" *-- The days of the week in Portuguese .DaysOfWeek(1) = "Domingo" .DaysOfWeek(2) = "Segunda-feira" .DaysOfWeek(3) = "Terça-feira" .DaysOfWeek(4) = "Quarta-feira" .DaysOfWeek(5) = "Quinta-feira" .DaysOfWeek(6) = "Sexta-feira" .DaysOfWeek(7) = "Sábado" Endwith *-- Finally, we return our useful object. Return loFoo EndProc EndDefine
Ok. Now, we could use our Utils class like this:
*-- Instantiate our "Utils" class. oUtils = Createobject("Utils") *-- Invoke the BringUtils() method. *-- Remember that it'll return that object created based on "Empty". oUtils = oUtils.BringUtils() *-- Now we can print the current Month's name in Portuguese... ? oUtils.Months(Month(Date())) *-- ...or the current day of the week date in Portuguese. ? oUtils.DaysOfWeek(Dow(Date()))
The Scatter command was enhanced
Now we can update an existent object using the Additive keyword of the Scatter command. See how useful it is:
*-- Let's create a simple Business object for "Customer" Define Class Customer As Session *-- Some properties... Company_Name = "" Contact_Name = "" *-- A simple method to play with the data. Procedure GiveMeInfo Return Alltrim(This.Company_Name) + " - " + This.Contact_Name Endproc Enddefine
And now, take a look at this:
*-- We instantiate our Customer object. oCustomer = CreateObject("Customer")
*-- We get some data... just for testing... Select Company_Name, Contact_Name From (_samples+"\tastrade\data\customer") into cursor curCustomer *-- We can now "scatter" data to the BizObject that´s already created. Scatter Name oCustomer additive *-- Invoke our GiveMeInfo() method. MessageBox(oCustomer.GiveMeInfo())
I think that's quite useful, don't you think?
Another nice new thing related to "scattered" objects is the fact that the INSERT-SQL statement now accepts data from an object. Therefore we could do something like this:
*-- Let's create a simple Test cursor. Create Cursor Foo (Company_Name C(30), Contact_Name C(30)) *-- Now, supposing that we have the object created on the last sample: Insert into Foo from name oCustomer
This sort of cool new thing just let VFP be even more powerful to play with data, mixing the use of business objects with our always handy VFP cursors.
Event Binding for native VFP objects
With VFP 7, we were presented with the EventHandler() function, that we can use to bind COM Objects events to a VFP object. Now we can do the same but for binding native VFP object events, using a brand new function called BindEvent().
Let's think...: we have a business object which contains a Data object, and we want to have any changes to that object monitored by another object. Given this scenario, let's create the business class first:
*-- Our BizObj. Define Class Customer As Session *-- A property for a Data object. oData = Null Procedure Init *-- Bring any data from the table. Select * From (_samples+"\tastrade\data\customer") into Cursor curCustomer *-- Create the data object. Scatter Name This.oData Memo EndProc EndDefine
Now we'll create our "Event Handler" class.
*-- We define a class. Define Class FooHandler As Session *-- We define a method that will handle *-- the "event" of a change for the "Company_Name" field.
*-- ...of course, on a real scenario we'd have a much more interesting thing here... Procedure CompanyNameHandling Messagebox("You´re changing the Company´s Name") EndProc EndDefine
And finally, let's use those classes:
*-- First, let's create our BizObj. oCustomer = CreateObject("Customer") *-- Now, our Event Handle's Object oFooHandler = CreateObject("FooHandler") *-- Finally, let's use the function Bindevent, in order to actually bind things here... Bindevent(oCustomer.oData, "Company_Name", oFooHandler, "CompanyNameHandling") *-- ...and it's time to try to change the value of "Company_name"... oCustomer.oData.Company_Name = "Anything..."
As you can see, the BindEvent() function receives as parameters the object that will fire the event (in this case the oCustomer.oData object), the property that will fire the event (in this case the Company_Name property), the object that will handle the event (in this case, our oFooHandler object), and finally, the method that will handle the event (in this case, the CompanyNameHandling() Method). Anytime that the Company_Name is changed, a messagebox will be displayed (as we have defined on the class).
Would you like to see a more "visual" example? If so, here it goes: let's say you have a form and you want it to always be resized when the _Screen is resized. We could do that like this:
*-- Let's create a simple form. Public oFooForm oFooForm = Createobject("FooForm") oFooForm.Visible = .T. *-- Let's bind things here... Bindevent(_Screen, "Resize", oFooForm, "Resize") *-- Take a look at our simple form. Define Class FooForm As Form Procedure Resize This.Top = _Screen.Top + 5 This.Left = _Screen.Left + 5 This.Width = _Screen.Width - 100 This.Height = _Screen.Height - 100 Endproc Enddefine
In the code above, we have defined that whenever the Resize event of the _Screen object got fired, we want that the Resize method of the oFooForm object to also be fired. As you may have noticed, the _Screen object is passive here, as it will never know that there's something "following its steps".
I know you like plenty of samples, so here goes another one, that will make you understand this even better this BindEvent() function.
Do you remember everytime you have needed to run some code whenever a textbox on a grid was clicked, no matter on which column? Until now, you did that by typing the code on every textbox's Click() method (urgh!), or by defining a textbox class which contains the desired code and then switching the textboxes on the grid with this class (and you don't like this approach that much, because you'll never use this class again in any other place...). Well, with the BindEvent() function, you'll now do something like this:
Let's explain: in the Init() method of the form I did the event binding. It was just a matter of iterating through all the Columns of the grid, and binding the RightClick() event of each textbox (the Control(2) on the grid), to the BringSomeInfo() method defined on the form. Isn't that pretty cool? Repeat after me: VFP Rocks!
The new Collection class
You don't need to create your own Collection classes anymore: now we have a native Collection class! Do you want a pratical sample for it? Here it goes: let's suppose that I have the need on my app to cascade all the forms that are currently opened. See how simple is to implement such a thing using the Collection class:
*-- Define a simple "FormsManager" class. Define Class FormsManager As Session *-- Define a property that will host a Collection. oFormsCollection = Null Procedure Init *-- Instantiate the Collection class. This.oFormsCollection = Createobject("Collection") Endproc
*-- Define a method that will add references for the Forms to the Collection. Procedure AddForm(loForm) *-- We can use the Add() method of the Collection object, *-- in order to add items to the collection. *-- In this sample, we´re determining the Form´s Caption *-- as the Key for the Item on the Collection. This.oFormsCollection.Add(loForm, loForm.Caption) *-- Bind Event to the QueryUnload of the Form, *-- this way the reference to the form is cleared by the *-- ReleaseForm method of this class. Bindevent(loForm, "QueryUnload", This, "ReleaseForm", 2) Endproc *-- Method to release the reference to the form that's being destroyed. Procedure ReleaseForm This.oFormsCollection.Remove(_Screen.ActiveForm.Caption) EndProc *-- A simple method to "cascade" the forms. Procedure CascadeForms Local lnTop, lnLeft lnTop = 5 lnLeft = 5 *-- We can use a ForEach structure to iterate *-- through all the items in the collection. For Each loForm In This.oFormsCollection loForm.Top = lnTop loForm.Left = lnLeft loForm.Visible = .T. lnTop = lnTop + 20 lnLeft = lnLeft + 20 Next Endproc Enddefine
The points that you should notice:
Now, we can use our "Form Manager" like this:
*-- Declare some variables... Public oForm1, oForm2, oForm3, oFM *-- Instantiate some forms, and give them captions... oForm1 = Createobject("Form") oForm1.Caption = "Customers"
oForm2 = Createobject("Form") oForm2.Caption = "Orders" oForm3 = Createobject("Form") oForm3.Caption = "Products" *-- Instantiate our "Forms Manager" oFM = Createobject("FormsManager") *-- Add the forms to our "Form Manager" oFM.AddForm(oForm1) oFM.AddForm(oForm2) oFM.AddForm(oForm3) *-- Ask the "Form Manager" to "Cascade" the Forms. oFM.CascadeForms()
The code above must produce this result:
Think: doing it like this, you have total control over your forms, and have an easy way to get a reference to any of them. And the Collection class provide us with some other cool features, like the ones below:
*-- We can query the Count property of the Collection object, *-- and discover how many items it contains. ? oFM.oFormsCollection.Count *-- We can access a single item on the collection by its Index... *-- ...for example, let's see the value of the Top property of the *-- form referenced as the second item within the collection. ? oFM.oFormsCollection.Item(2).Top *-- ...or by its "key" ? oFM.oFormsCollection.Item("Orders").Top
Another cool enhancement for the Insert-SQL
Aside from the previously mentioned new feature of Inserting new records from an object, we are now able to insert from a Select-SQL result. Look:
*-- Let's create a simple cursor... Create Cursor MyCustomers (cName C(30)) *-- ...and insert rows returned from a Select-SQL statement. Insert into MyCustomers ; Select Company_Name ; from (_Samples+"\Tastrade\Data\Customer") ; where country = "Brazil"
Enhancements to Textmerge
I'm the one who always enjoy using Textmerge. And I love this new enhancement I am going to tell you about. Back in the July issue of UTMag/RapoZine, in the Shortcuts column, I told you about how we can use the Textemerge to build SQL statements in a cleaner way, instead of those awful string concatenations. Something like this:
Local lcSQL Text to lcSQL TextMerge NoShow Select Field1, Field2, Field3, Field4 From Table Inner Join AnotherTable On Table.SomeField = AnotherTable.SomeField Inner Join FooTable On Table.Somefield = FooTable.SomeField Where Field1 = << lcSomeVar >> And Field2 = << lcAnotherVar >> Group by Field3, Field2 Order by Field1 EndText
With that we had one drawback: we needed to remove white spaces with something like this:
StrTran(lcSQL, Chr(13)+Chr(10), " ")
Well, we don't need that anymore. We can just change the Text instruction to something like this:
Text to lcSQL TEXTMERGE NOSHOW PRETEXT 1+2
With the new PRETEXT keyword we are able to remove white-spaces and tabs (flags 1+2) at the end of each line. This is not just useful for SQL statements, but for XML documents as well.
Conclusion
Ok, that´s it! As I told you at the begining of this article, VFP 8 brings to us a lot of cool new features, and with this article I just showed you some cool new "non-visual" features. I hope you liked it. Take a look on the other articles in this issue related to this exciting Beta of VFP 8!