Sei sulla pagina 1di 20

FoxTalk 2.

0
Solutions for Microsoft® Visual FoxPro® Developers

I Got Rendered
Where?
Doug Hennig 9.0

Doug Hennig continues his discussion of VFP 9 report listeners by presenting a set
of classes that output the contents of a report run to a cursor and use that cursor to
provide a “live” report preview surface.

W
HEN you run a report in VFP 8 or earlier, the output is sort of a black
box: You have little control over the preview window, don’t have any
information about what got rendered where, and can’t provide a
Sample Issue
“live” preview surface (one in which click events can be trapped to perform
1 I Got Rendered Where?
some object-specific action). Doug Hennig
Over the past two months, I’ve discussed the new ReportListener class in
VFP 9 and how it can be used to control report output in ways that previously 7 Accessing Hotmail and MSN
Accounts in Visual FoxPro
weren’t possible. The ReportListener subclasses I’ve shown so far performed
Anatoliy Mogylevets
some type of visible output, such as HTML, reports with dynamic formatting,
and so forth. 14 Tips from the VFP Team
This month, the output from a listener won’t really go anywhere obvious The Microsoft Visual FoxPro Team
to the user; instead, it’s sent to a cursor so we can track what got rendered
15 The Kit Box: SET Things Right
where. Having this information provides all kinds of interesting uses, such as Andy Kramek and Marcia Akins
a live preview surface, a dynamically generated table of contents, the ability to
find text, conditionally highlighting certain report objects, and so on. 20 Sample Issue Downloads

DBFListener
DBFListener, contained in DBFListener.PRG, is a subclass of _ReportListener,
9.0 8.0 7.0 6.0
a report listener subclass defined in _ReportListener.VCX in the FFC
subdirectory of the VFP home directory. I discussed _ReportListener in my Applies to Applies to Applies to Applies to
VFP 9.0 VFP 8.0 VFP 7.0 VFP 6.0
February 2005 FoxTalk 2.0 column, “Listening to a Report.” Because it’s a
subclass of _ReportListener, DBFListener can be used either as the sole
listener for a report or as one of a chain of listeners, each of which
performs some function during the report run. The ListenerType property Applies to Accompanying files available online at
VFP 5.0 www.pinnaclepublishing.com
Continues on page 3
AD:
AGE
LL P IELD
FU
O NE
F
ST

2 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


I Got Rendered Where?... passed in Unicode, it has to be converted back to normal
text using STRCONV() to make it useful. Render uses the
Continued from page 1 helper methods SetFRXDataSession and ResetDataSession
defined in its parent class to switch to the data session the
of DBFListener is set to 3 to suppress output to a preview FRX cursor is in and back again. This allows Render to
window or printer. get the OBJTYPE and OBJCODE values from the FRX for
Just before the report is run, the code in the the current object. Note: This code doesn’t currently do
BeforeReport event creates a cursor or table to hold the anything special with images because I haven’t decided
rendered report contents. To create a table, set the what to do with them yet.
lUseCursor property to .F. and cOutputDBF to the name
and path of the table to create (if you don’t specify the function Render(tnFRXRecNo, tnLeft, tnTop, tnWidth, ;
tnHeight, tnObjectContinuationType, ;
name, a SYS(2015) name is used in the Windows temp tcContentsToBeRendered, tiGDIPlusImage)
directory). To create a cursor, set lUseCursor to .T. and local lcContents, ;
liObjType, ;
cOutputAlias to the alias to use for the cursor (if you liObjCode
with This
don’t specify the alias, a SYS(2015) name is used). if empty(tcContentsToBeRendered)
In either case, the table or cursor has columns for the lcContents = ''
else
record number in the FRX for the report object, the lcContents = strconv(tcContentsToBeRendered, 6)
OBJTYPE and OBJCODE values from the FRX (which endif empty(tcContentsToBeRendered)
.SetFRXDataSession()
indicate what type of object it is), the left, top, width, and go tnFRXRecno in FRX
height of the rendered object, the “continuation type” liObjType = FRX.OBJTYPE
liObjCode = FRX.OBJCODE
parameter passed to the Render method (see the VFP .ResetDataSession()
Help topic for Render for a discussion of this parameter), insert into (.cOutputAlias) ;
values (tnFRXRecNo, liObjType, liObjCode, ;
the contents of the object if it’s a field or label, and the tnLeft, tnTop, tnWidth, tnHeight, ;
number of the page on which it appears. tnObjectContinuationType, lcContents, ;
.PageNo)
endwith
function BeforeReport endfunc
local lcTable
with This
The Destroy method (not shown here) closes
* If a table name and/or an alias wasn't specified, the cursor or table and deletes the table if the
* create default names.
lDeleteOnDestroy property is .T.
if empty(.cOutputDBF)
.cOutputDBF = addbs(sys(2023)) + sys(2015) + ;
When DBFListener is used as the listener for a
'.dbf' report, nothing appears to happen; the report isn’t
endif empty(.cOutputDBF)
if empty(.cOutputAlias) previewed, printed, output to HTML, or anything else.
.cOutputAlias = ; However, after the report run is complete, a cursor or
strtran(juststem(.cOutputDBF), ' ', '_')
endif empty(.cOutputAlias) table is available containing information about each
report element and where it was rendered.
* If the cursor is already open, close it. If the
* table already exists, nuke it.

use in select (.cOutputAlias)


SFPreview
if file(.cOutputDBF) Once you have the rendered content of a report in a
erase (.cOutputDBF)
erase forceext(.cOutputDBF, 'FPT') cursor or table, you can use it for lots of things. I’ll show
erase forceext(.cOutputDBF, 'CDX') you a couple of uses for it in this article.
endif file(.cOutputDBF)
SFPreviewForm is a form class providing a report
* Create either a cursor or a table. preview dialog with different capabilities than the
lcTable = iif(.lUseCursor, 'cursor ' + ; preview window that comes with VFP. It raises events
.cOutputAlias, 'table ' + .cOutputDBF) when report objects are clicked and supports other
create &lcTable (FRXRECNO I, OBJTYPE I, ;
OBJCODE I, LEFT I, TOP I, WIDTH I, HEIGHT I, ; capabilities, such as finding text. It has to use some
CONTTYPE I, CONTENTS M nocptrans, PAGE I)
index on PAGE tag PAGE
trickery to do this: Since a preview page is a GDI+ image,
endwith nothing specific happens when you click on some text
* Do the usual behavior. in the image.
SFPreviewForm supports report object events by
dodefault()
endfunc creating a shape object on the preview surface for every
rendered object. These shapes can, of course, capture
As each object in the report is rendered, the Render events such as mouse movement or clicks, making it
event fires. The code in this event in DBFListener adds a possible to have a live preview surface. The shapes aren’t
record to the cursor. Since the text of a field or label is added to the form itself, but to a container that sits on the

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 3


form. Since a different set of shapes must be created for library are specified in the cContainerClass and
each page as you navigate through the preview, it’s easier cContainerLibrary properties.
to delete the container (which deletes all of the shapes DisplayPage then ensures that a valid page number
at once) and create a new one than to remove each was specified and spins through the rendered output
individual shape prior to adding new ones. cursor, adding a shape for each object on the current
The main method in SFPreviewForm is DisplayPage. page to the container. It then sets the nCurrentPage
This method displays the current page of the report and property to the page number and calls DrawPage to
creates shape objects in the same size and position as each display the preview image for the current page on the
report object on the page. How does DisplayPage know form. DisplayPage updates lFirstPage and lLastPage so
what report objects appear on the page? By looking in the the buttons in a toolbar can be properly enabled or
cursor created by DBFListener, of course. disabled (for example, the Last Page button is disabled if
lLastPage is .T.), and then refreshes the toolbar.
lparameters tnPageNo InitializePreview, which is called from DisplayPage
local lnPageNo, ;
lcObject, ; the first time that method is called, ensures that certain
loObject
with This
properties are initialized properly. As the comments in
this method indicate, one complication is that if you use a
* If we haven't been initialized yet, do so now.
RANGE clause for a report run, such as RANGE 6, 7, the
if vartype(.oListener) = 'O' pages may be numbered 6 and 7 but when you call the
if not .lInitialized
.InitializePreview() listener’s OutputPage method to draw the preview image
endif not .lInitialized on the form, the first page is 1, the second page is 2, and
* Ensure we have a shape container with no shapes. so forth. To overcome the potential mismatch between
.AddShapeContainer()
these numbering schemes, InitializePreview sets the
nFirstPage and nLastPage properties to the first and last
* Ensure a proper page number was specified.
page numbers (6 and 7 in this example) and nPageOffset
if between(tnPageNo, .nFirstPage, .nLastPage) as the value to subtract from a “real” page number to get
lnPageNo = tnPageNo
else the output page number.
lnPageNo = .nFirstPage InitializePreview also puts the report page height and
endif between(tnPageNo, .nFirstPage, .nLastPage)
width into the nMaxWidth and nMaxHeight properties.
* Select the output cursor and create a shape around These values are used to size the container used for the
* each report object on the specified page.
report preview; if they’re larger than the form size,
select (.cOutputAlias)
seek lnPageNo
scrollbars will appear because the form’s ScrollBars
scan while PAGE = lnPageNo property is set to 3-Both.
.AddObjectToContainer()
endscan while PAGE = lnPageNo Note a couple of complications here. First, the page
height and width values are in 960ths of an inch, while
* Set the current page number and draw the page.
the form uses pixels. Fortunately, it’s easy to convert
.nCurrentPage = lnPageNo from 960ths of an inch to pixels: Divide the value by 10,
.DrawPage()
since the report engine renders at 96 DPI. The second
* Flag whether we're on the first or last page. complication is that if the DBFListener object isn’t the
.lFirstPage = lnPageNo = .nFirstPage lead listener for a report run, its GetPageWidth and
.lLastPage = lnPageNo >= .nLastPage
GetPageHeight methods don’t return valid values.
* Refresh the toolbar if necessary. Fortunately, _ReportListener handles this by setting the
.RefreshToolbar() custom SharedPageWidth and SharedPageHeight
properties to the appropriate values.
* If we don't have a listener object, we can't
* proceed. Finally, InitializePreview clears some properties used
for finding text (we’ll look at those later), opens the class
else
messagebox('There is no listener object.', 16, ; library used for the shapes that will be added to the form
.Caption)
endif vartype(.oListener) = 'O'
for the report objects, and flags that initialization has been
endwith done so this method isn’t called a second time.

This code starts by calling InitializePreview if the with This


preview hasn’t been initialized yet, and then calling * Set the starting and first page offset. Even though
AddShapeContainer to add the container used to hold the * we may not have output the first page due a RANGE
* clause, the pages are numbered starting with 1 from
shapes to the form. We won’t look at AddShapeContainer * an OutputPage point-of-view.
here; it simply removes any existing container and .nFirstPage = .oListener.CommandClauses.RangeFrom
adds a new one from the class whose class name and .nPageOffset = .nFirstPage - 1

4 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


* The Width and Height values are 1/10th of the current report object. The shape is sized and positioned
* values from the report because those values are in
* 960ths of an inch and the report engine uses a based on the HEIGHT, WIDTH, TOP, and LEFT columns
* resolution of 96 DPI. Our listener may be a
* successor, so use the appropriate Shared properties
in the cursor, although, as we saw earlier, these values
* if they exist. Also, get the last page number using must be divided by 10 to convert them to pixels.
* either SharedOutputPageCount (which may not have
* been filled in if the listener is the lead listener
* and has no successor) or OutputPageCount, adjusted local lcObject, ;
* for the offset. loObject
with This
if pemstatus(.oListener, 'SharedPageWidth', 5) lcObject = 'Object' + transform(recno())
.nMaxWidth = .oListener.SharedPageWidth/10 .oContainer.AddObject(lcObject, .cShapeClass)
.nMaxHeight = .oListener.SharedPageHeight/10 loObject = evaluate('.oContainer.' + lcObject)
if .oListener.SharedOutputPageCount > 0 with loObject
.nLastPage = ; .Width = WIDTH/10
.oListener.SharedOutputPageCount + ; .Height = HEIGHT/10
.nPageOffset .Top = TOP/10
else .Left = LEFT/10
.nLastPage = .oListener.OutputPageCount + ; .nRecno = recno()
.nPageOffset .Visible = .T.
endif .oListener.SharedOutputPageCount > 0 endwith
else endwith
.nMaxWidth = .oListener.GetPageWidth()/10 return loObject
.nMaxHeight = .oListener.GetPageHeight()/10
.nLastPage = .oListener.OutputPageCount + ;
.nPageOffset Handling events
endif pemstatus(.oListener, 'SharedPageWidth', 5) The cShapeClass property is set to “SFReportShape” by
* Clear the find settings. default. SFReportShape is a subclass of Shape with code
in its Click, RightClick, and DblClick events that call the
.ClearFind()
OnObjectClicked method of the form, passing it the
* Open the appropriate class library if necessary. record number in the report contents cursor represented
if not '\' + upper(.cShapeLibrary) $ ; by this shape and a numeric value indicating which event
set('CLASSLIB') occurred (1 for Click, 2 for DblClick, or 3 for RightClick).
.lOpenedLibrary = .T.
set classlib to (.cShapeLibrary) additive This allows SFPreviewForm to receive notification
endif not '\' ...
whenever a report object is clicked.
* Flag that we've been initialized. OnObjectClicked handles a click on the shape by
.lInitialized = .T. raising the appropriate event for the click type. The
endwith benefit of using RAISEEVENT() is that any object can use
BINDEVENT() to ObjectClicked, ObjectDblClicked, or
DrawPage, called from DisplayPage to draw the ObjectRightClicked to implement the desired behavior
current preview page image on the form, calls the without having to subclass SFPreviewForm. You could
OutputPage method of the listener, passing it the page even have multiple behaviors if you wish, since multiple
number (adjusted for the starting offset), the container objects can bind to the same event. Any object that
used as the placeholder for the image, and the value 2, binds to these events will receive as a parameter a
which indicates the output should go to a VFP control. SCATTER NAME object for the current record in the
DrawPage also calls HighlightObjects to highlight any report contents cursor.
report objects we want highlighted; I’ll discuss this later.
Note that the Paint event of the form also calls DrawPage lparameters tnRecno, ;
tnClickType
because when the form is redrawn (such as during a local loObject
resize), the placeholder container is redrawn and therefore select (This.cOutputAlias)
go tnRecno
the preview image is lost, so DrawPage restores it. scatter memo name loObject
do case
with This case tnClickType = 1
if vartype(.oListener) = 'O' raiseevent(This, 'ObjectClicked', loObject)
.oListener.OutputPage(.nCurrentPage - ; case tnClickType = 2
.nPageOffset, .oContainer, 2) raiseevent(This, 'ObjectDblClicked', loObject)
.HighlightObjects() otherwise
else raiseevent(This, 'ObjectRightClicked', loObject)
messagebox('There is no listener object.', 16, ; endcase
.Caption)
endif vartype(.oListener) = 'O'
endwith There are several other methods in SFPreviewForm.
Show instantiates a toolbar using the class and library
AddObjectToContainer, called from DisplayPage, names specified in the cToolbarClass and cToolbarLibrary
adds a shape of the class specified in cShapeClass (the properties if lShowToolbar is .T. The FirstPage,
class library specified in cShapeLibrary was previously PreviousPage, NextPage, and LastPage methods call
opened in InitializePreview) to the shape container for the DisplayPage, passing the appropriate value to display

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 5


the desired page. SaveFormPosition
and SetFormPosition save and
restore the size and shape of the
preview form between report runs.
I’ll discuss the rest of the methods
next month.

Live preview surface


Let’s try it out. TestSFPreview.PRG
uses DBFListener as the listener for
the Customers report, and then
instantiates SFPreviewForm, sets its
properties to the appropriate values,
and tells it to display the first page. Figure 1. SFPreviewForm, combined with DBFListener, provides a live preview surface.

loListener = newobject('DBFListener', ; When you run TestSFPreview.PRG, you’ll see the


'DBFListener.PRG')
report form Customers object loListener preview form shown in Figure 1. Although this looks
* Show the report in our custom previewer. similar to the preview form that comes with VFP, try
clicking on various report objects. You’ll see information
loForm = newobject('SFPreviewForm', 'SFPreview.vcx')
with loForm about the object echoed to the Debug Output window
.cOutputAlias = loListener.cOutputAlias
.Caption = 'Customer Report' (I decided to send output there rather than WAIT
.oListener = loListener WINDOW because the latter interfered with the DblClick
.FirstPage()
endwith event). This simple example doesn’t do much, but
imagine the possibilities: jumping to another section of
TestSFPreview.PRG then instantiates an object to the report or a different report altogether, launching a
handle clicks in the preview surface and binds the various VFP form, providing a shortcut menu that displays
click events to it. Finally, it displays the Debug Output different options depending on the particular report
window (because that’s where click events will be echoed object that was right-clicked, supporting bookmarks,
by the click handler class) and shows the preview form. and so on.
You may have noticed that the toolbar contains a
loHandler = createobject('ClickHandler')
bindevent(loForm, 'ObjectClicked', loHandler, ; couple of interesting-looking buttons. These are used
'OnClick')
bindevent(loForm, 'ObjectDblClicked', loHandler, ; for finding text within the report. I’ll discuss that topic
'OnDblClick') next month.
bindevent(loForm, 'ObjectRightClicked', loHandler, ;
'OnRightClick')

* Display the debug output window and the preview Summary


* form. By outputting the contents of a report run to a table or
activate window 'debug output' cursor, DBFListener provides us with information about
loForm.Show(1)
where each object is rendered on a report, which can be
used in a lot of ways. I hope you’re starting to see the
Here’s part of the definition of the click handler class
incredible possibilities the VFP 9 ReportListener class
(the OnDblClick and OnRightClick methods aren’t shown
provides us. ▲
because they’re nearly identical to OnClick). The ObjType
property of the passed object indicates what type of report
object was clicked (for example, 5 means a label and 8 504HENNIG.ZIP at www.pinnaclepublishing.com
means a field), and Contents contains the contents in the
case of a label or field. Doug Hennig is a partner with Stonefield Systems Group Inc. He’s the
author of the award-winning Stonefield Database Toolkit (SDT) and
define class ClickHandler as Custom Stonefield Query, and the MemberData Editor, Anchor Editor, New
procedure OnClick(toObject)
do case Property/Method Dialog, and CursorAdapter and DataEnvironment
case inlist(toObject.ObjType, 5, 8) builders that come with VFP. He’s a co-author of the What’s New in Visual
debugout 'You clicked ' + ;
trim(toObject.Contents) FoxPro series and The Hacker’s Guide to Visual FoxPro 7.0, all from
case toObject.ObjType = 7 Hentzenwerke Publishing. Doug has spoken at every Microsoft FoxPro
debugout 'You clicked a rectangle'
case toObject.ObjType = 6 Developers Conference (DevCon) since 1997 and at user groups and
debugout 'You clicked a line' developer conferences all over North America. He’s a long-time Microsoft
case toObject.ObjType = 17
debugout 'You clicked an image' Most Valuable Professional (MVP), having first been honored with this
endcase award in 1996. www.stonefield.com, www.stonefieldquery.com,
endproc
enddefine dhennig@stonefield.com.

6 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


FoxTalk 2.0

Accessing Hotmail and MSN


Accounts in Visual FoxPro
Anatoliy Mogylevets 8.0 9.0

In some cases, it’s possible to send and receive Hotmail through a long series of clicks in a browser window.
messages with Visual FoxPro code. In this article, Anatoliy Outlook Express can integrate Hotmail accounts, and
Mogylevets describes several Visual FoxPro classes that on the surface, OE’s handling of the e-mail account is
use Microsoft ServerXMLHTTP and XMLDOM objects to almost indistinguishable from a regular SMTP/POP3
implement WebDAV access to the Hotmail server. You’ll also account. Therefore, we could deduce that there’s a way for
learn some of the mysteries of HTTP requests, including a non-browser application to interact with the Hotmail
request headers. server. If you have a packet sniffing application installed
on your computer, it’s easy to see what’s going on when a

T
HE system requirements for what’s described in this Hotmail account is synchronized through Outlook Express.
article are Windows XP/2000/2003, Visual FoxPro A packet sniffer captures all TCP packets that leave or
version 8 or 9, Microsoft ServerXMLHTTP and enter your computer and can help you understand HTTP,
XMLDOM COM objects, and a Hotmail or MSN account. FTP, SMTP, POP3, and other protocols. I have a packet
Note that the functionality described here may not be sniffer installed on my computer, so I set it to the TCP
available for free Hotmail accounts. packet capturing mode and pressed the Send/Recv button
on the Outlook Express toolbar. Figure 1 shows the result.
Introduction As you can see, the local computer connects to port 80
Anyone can easily access a Hotmail account using on three remote servers consecutively. There’s the first
the standard MSN Web page, which has a very clue—this is the HTTP protocol. By the way, you may
straightforward interface. But, can you automate or “nslookup” the IP addresses to find out who they belong
integrate that access in a FoxPro application? You could, to. Let’s see what’s inside those TCP packets.
of course, host a browser control on a form, but to check Figure 2 shows that this is an HTTP request sent by
new e-mails or to send a message you’d still have to go the local computer to a remote server at services.msn.com.

Figure 1. Outlook Express exchanges TCP packets with the Figure 2. Outlook Express communicates with the Hotmail server
Hotmail server. using HTTP requests.

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 7


You may know about HTTP GET and POST requests, but to the server over a TCP/IP connection. The first line of
what is the PROPFIND request? And why is the body of the HTTP request includes the verb (also called the
the request in XML format? method), the identifier of the Internet resource (URI), and
A Google search on PROPFIND quickly reveals its the version of the HTTP protocol.
relation to the WebDAV (Web Distributed Authoring The list of verbs includes GET, POST, HEAD, PUT,
and Versioning) protocol. So now I know the ground on TRACE, and other values defined in the HTTP/1.1
which the Outlook Hotmail client stands. The WebDAV specification. WebDAV adds a new set of verbs like
protocol is an extension to the HTTP/1.1 protocol and PROPFIND, PROPPATCH, MOVE, DELETE, and others.
allows you to read and modify remote data or to build The verb indicates the action to be performed on the
Web applications that are writable. resource identified by the URI.
The WebDAV client (Outlook Express) sends HTTP The next part of the request contains the header
requests to the WebDAV server (Hotmail server) in an fields, commonly called “request headers.” The client uses
attempt to trigger an activity on the server. The client them to pass to the server information about the request
encodes the information in the request’s HTTP headers and about the client itself.
as well as in its XML-formatted body. The optional body section ends the request. The body
The server decodes the command and performs the can include a file that the client posts to the server or a
requested operation, such as purging deleted messages, block of formatted data (text-delimited, XML, HTML, and
then sends a response back to the client. A typical so on). As you can imagine, the size of the body can be
response is shown in Figure 3. really big. Here’s an example. When I open my browser, it
From the response, the WebDAV client can figure sends an HTTP request to Google, which currently is my
out whether the requested operation has succeeded. home page.
After watching my packet sniffer while working with From Figure 4, you can see that the GET verb is used
my Hotmail accounts for awhile, I soon had enough and the URI is “/” on www.google.ca. By reading the
captured packets to analyze the client-server interaction User-Agent request header, the server “knows” the OS
and replicate it in Visual FoxPro code. and the browser used on the client. Some Web servers
At this point, I must state that Hotmail WebDAV build their responses based on the User-Agent value to
access is undocumented. Period. PC World magazine tailor them to the client’s hardware and software. That’s
recently estimated the number of active Hotmail accounts why the Google search page on a Pocket PC looks
using the WebDAV feature as 9.4 million. Still, the different than on a desktop computer.
protocol is undocumented and Microsoft seems to be in Note that this request has no body section, because
the process of making this feature available only to GET requests aren’t supposed to have one. Served with
paying Hotmail users due to abuse by spammers. this request, Google sends back to the browser the HTML
Generally, if you can access your account with Outlook code of its search page.
Express, you should be able to do the same with the VFP There are several ways of opening and sending
code in this article. direct and indirect HTTP requests in Visual FoxPro code:
using API libraries WinINet, WinHTTP, Winsock, and
A few words about HTTP requests URL Monikers; using the Winsock Control; or using a
On the Internet, a request is a message sent by the client third-party library or control. For the WebmailClient
class described in this article, I picked the Microsoft
ServerXMLHTTP COM object. More accurately, I couldn’t
find anything else to use except this one.
You could instead try using the MSXML2.XMLHTTP
object, though in this case your FoxPro code would
have to implement the server part (redirection and
authorization). The Microsoft ServerXMLHTTP object is

Figure 3. The Hotmail server answers the client’s HTTP request by Figure 4. The HTTP request a browser sends to retrieve the
sending a response comprised of headers and XML body. Google search page.

8 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


included with the Microsoft XML Parser (MSXML),
THIS.xmlGateway = CREATEOBJECT(;
version 3.0 or later. "Microsoft.XMLDOM")
* code skipped here

Using the Microsoft ServerXMLHTTP and PROCEDURE ConnectTo(cEmail, cPwd)


LOCAL cServer
XMLDOM objects THIS.email = LOWER(ALLTRIM(m.cEmail))
The ServerXMLHTTP object is ideal for sending various THIS.pwd = LOWER(ALLTRIM(m.cPwd))

HTTP requests to remote servers and receiving responses. DO CASE


That’s all you need to build the data exchange part of a CASE "@hotmail.com" $ THIS.email
cServer = URL_HOTMAIL
Hotmail client in Visual FoxPro. A downside is that this CASE "@msn.com" $ cEmail
object isn’t supported on Windows 95/98/Me computers. cServer = URL_MSN
OTHERWISE
This is how an instance of the ServerXMLHTTP object * invalid email account
RETURN .F.
is created: ENDCASE

oHttp = CreateObject("MSXML2.ServerXMLHTTP") * Connecting to Hotmail server...


THIS.SendPropfind(cServer, XML_GATEWAY)

The object supports XML data exchange and has IF THIS.response != RESPONSE_OK
* failed to connect
“server” in its name—and it really works as a server by RETURN .F.
establishing a connection with Hotmail, redirecting, ENDIF

creating cookies, and encrypting and decrypting data. * load the response to xml parser
THIS.xmlGateway.Load(THIS.http.ResponseXml)
Another Microsoft object, XMLDOM, is used to parse RETURN .T.
the XML data received from the Hotmail server.
PROCEDURE SendPropfind(href, xml)
THIS.SendHttpRequest(PROPFIND, m.href,;
Visual FoxPro classes wrapping the Hotmail "Cache-Control: no-cache;" +;
"Content-Type: text/xml", m.xml)
WebDAV functionality
The class library described in this article contains four
classes inherited from the VFP Session class (see Table 1).

Connecting to the mail server


Until now, I’ve been using the word “Hotmail” solely for
simplicity, meaning both Hotmail and MSN e-mail
accounts. The only difference between them is the URL
of the mail server.

#DEFINE URL_MSN http://oe.msn.msnmail.hotmail.com/cgi-


bin/hmdata
#DEFINE URL_HOTMAIL http://services.msn.com/svcs/hotmail/
httpmail.asp

To open communication with the server, the client


sends a PROPFIND HTTP request along with the XML
body containing a template for the server’s response.
Figure 5 shows this request for a Hotmail account.
Here’s the FoxPro code in several methods of the
WebmailClient class involved in this HTTP request:

THIS.http = CREATEOBJECT(;
Figure 5. The first HTTP request the Hotmail client sends to the
"MSXML2.ServerXMLHTTP") Hotmail server when initiating a connection.

Table 1. The FoxPro class library that wraps the Hotmail WebDAV functionality.

Class Description
WebmailClient Connects to the Hotmail server and requests a list of folders. It has methods for sending HTTP requests. Knows how to clear the
Deleted Items folder. Knows how to send an e-mail. The FOLDERS property is a collection of WebmailFolder objects.

WebmailFolder Represents a Hotmail folder. The class has a method that retrieves the list of message envelopes stored in this folder. The
MESSAGES property is a collection of WebmailMessage objects.

WebmailMessage Represents a Hotmail message. Can retrieve the message, delete the message, or move it to another folder. The HEADERS property
is a collection of message headers. For simplicity, the object doesn’t parse the message body’s parts or attachments.

MessageHeader Represents a message header like From, To, Cc, Content-Type, Return-Path, and so on.

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 9


PROCEDURE SendHttpRequest The Hotmail server responds with the headers (see
LPARAMETERS cMethod, href,cHeaders, cBody)
STORE "" TO THIS.response, THIS.statustext,; Figure 6) and some XML data (see Figure 7). Note that
THIS.responseheaders
the X-Dav-Error header’s value is “200 No error”. All
LOCAL ex As Exception successful Hotmail transactions have this value returned
WITH THIS.http
TRY from the server.
.Open(cMethod, m.href, 0,; As you can see from the code, THIS.xmlGateway
THIS.email, THIS.pwd)
CATCH TO ex (XML parser) loads the XML part of the response that
ENDTRY contains links to the special folders including Inbox, Sent
IF VARTYPE(m.ex) = "O" Messages, Deleted Items, and others. We’ll use some of
THIS.errorno = ex.ErrorNo these links later in other HTTP requests.
THIS.errormessage = ex.Message
RETURN .F.
ENDIF
Requesting the list of folders
THIS.AddHeaders(cHeaders) The same PROPFIND verb is used to retrieve the list of
TRY folders for the Hotmail account. The XML body of the
.Send(m.cBody) request is actually the list of folder properties, which
THIS.response = SUBSTR(;
.GetResponseHeader(X_DAV_ERROR),1,3) provides a template for the Hotmail server (see Figure 8).
THIS.statustext = THIS.http.StatusText The server will populate this template with the properties
THIS.responseheaders =;
THIS.http.GetAllResponseHeaders() of each folder and return it in the body of the response.
CATCH
ENDTRY
Here’s the WebmailClient code to handle this request:

IF VARTYPE(m.ex) = "O" THIS.xmlFolders = CREATEOBJECT(;


THIS.errorno = ex.ErrorNo "Microsoft.XMLDOM")
THIS.errormessage = ex.Message
RETURN .F. THIS.folders = CREATEOBJECT("Collection")
ENDIF
ENDWITH * code skipped here

PROCEDURE GetFolders
* reads available folders and populates
* a collection of WebmailFolder objects
= ClearCollection(THIS.folders)

LOCAL cFoldersHref, xmlFolder,;


nFolderCount, nFolderIndex, oFolder

cFoldersHref = THIS.xmlGateway.;
SelectSingleNode(MSGFOLDERROOT_NODE).Text

THIS.SendPropfind(m.cFoldersHref, XML_FOLDERS)
IF THIS.response <> RESPONSE_OK
RETURN .F.
ENDIF

THIS.xmlFolders.Load(THIS.http.ResponseXml)

nFolderCount = THIS.xmlFolders.;
Figure 6. The header fields of the Hotmail server’s response. Note
the X-Dav-Error header.

Figure 7. The XML body of the Hotmail server’s response. This Figure 8. The Hotmail client sends this HTTP request to get the
particular response returns main URIs for the server and for the list of folders for the Hotmail account.
Hotmail account.
10 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com
SelectSingleNode(MULTISTATUS).; GetMessageEnvelopes method of the WebmailFolder class:
ChildNodes.Length

FOR nFolderIndex=0 TO nFolderCount-1 PROCEDURE GetMessageEnvelopes


xmlFolder = THIS.xmlFolders.; = ClearCollection(THIS.messages)
SelectSingleNode(MULTISTATUS).;
ChildNodes(nFolderIndex) THIS.webmail.SendPropfind(THIS.href,;
XML_MESSAGES)
oFolder = CREATEOBJECT("WebmailFolder",;
IF THIS.webmail.response != RESPONSE_OK
THIS, xmlFolder)
RETURN
THIS.folders.Add(oFolder,; ENDIF
oFolder.foldername)
LOCAL xmlMessages, xmlMessage, nMessageCount,;
oFolder=Null nMessageIndex
NEXT xmlMessages = CreateObject("Microsoft.XMLDOM")
WITH xmlMessages
.Load(THIS.webmail.http.ResponseXml)
In the return message, each folder is described by nMessageCount = .SelectSingleNode(;
MULTISTATUS).ChildNodes.Length
its name, the URI of the folder, the number of stored
messages, and other parameters. Currently the Hotmail FOR nMessageIndex=0 TO nMessageCount-1
xmlMessage = .SelectSingleNode(;
server supports only one level of folders, so the HASSUBS MULTISTATUS).ChildNodes(;
and NOSUBS properties are irrelevant. Using the XML nMessageIndex)

parser, the WebmailClient object populates its FOLDERS oMessage = CREATEOBJ("WebmailMessage",;


collection of WebmailFolder objects. The following code THIS, xmlMessage)
THIS.messages.Add(oMessage,;
shows the properties defined for the WebmailFolder class: oMessage.msgid)
oMessage = Null
NEXT
DEFINE CLASS WebmailFolder As Session
ENDWITH
webmail=Null
href=""
foldername=""
specialfolder=.F. The server responds with the headers and an XML
messagecount=0 body holding the list of message envelopes. Using the
unreadcount=0
messages=0 && collection XML parser, the WebmailFolder object populates its
PROCEDURE Init(oWebmail, xmlFolder)
MESSAGES collection of WebmailMessage objects.
* code skipped here
ENDDEFINE DEFINE CLASS WebmailMessage As Session
folder=Null
href=""
Reading the envelopes of the messages stored msgid=""
in a folder readstatus=.F.
sender=""
The WebmailFolder class includes the recipient=""
GetMessageEnvelopes method that reads the message subject=""
created=""
envelopes. Because it doesn’t retrieve each entire contentlength=0
message, it takes a comparatively short time. The HTTP rawcontent=""
rawheaders=""
request again has the PROPFIND verb, and the XML headers=0
body of the request is a template with the list of properties PROCEDURE Init(oFolder, xmlMessage)
for the message envelope (see Figure 9). Here’s the * code skipped here
ENDDEFINE

Retrieving a message
The WebmailMessage class includes the
GetMessageContent method that retrieves the entire
message. Unlike the previous requests, this one uses the
GET verb and has no XML body. The URI of the message
is passed in the request’s first line right after the verb, and
the body of the Hotmail server’s response contains the
raw message content. Here’s the GetMessageContent
method of the WebmailMessage class:

PROCEDURE GetMessageContent
= ClearCollection(THIS.headers)
LOCAL cResponseText, nPos

THIS.folder.webmail.SendHttpRequest("GET",;
THIS.href, "", "")

IF THIS.folder.webmail.response = RESPONSE_OK
Figure 9. The Hotmail client sends this HTTP request to get cResponseText =;
THIS.folder.webmail.http.ResponseText
the list of message envelopes in the INBOX folder of the nPos = AT(dCRLF, cResponseText)
Hotmail account. THIS.rawcontent = SUBSTR(cResponseText,;

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 11


nPos+4) the request doesn’t need an XML body because all
THIS.rawheaders = SUBSTR(cResponseText,;
1, nPos) required information is already included in the top line
THIS.ParseHeaders
and the headers.
ELSE
STORE "" TO THIS.rawheaders,; PROCEDURE DeleteMessage
THIS.rawcontent LOCAL href
ENDIF href = THIS.folder.webmail.GetTrashHref()
RETURN THIS.MoveMessage(m.href)

In Outlook Express, you can see the raw content PROCEDURE MoveMessage(cDstHref)
of a message by clicking on its Properties | Details | LOCAL cHeaders
cHeaders = "Destination: " + cDstHref +;
Message Source. THIS.msgid + ";Allow-Rename: t"
The top part of the raw message contains various WITH THIS.folder.webmail
message parameters. The following part, the body of the .SendHttpRequest("MOVE",;
THIS.href, cHeaders, "")
message, may not be as simple as shown in Figure 10. RETURN (.response = RESPONSE_OK)
Very often the body includes several parts (plain text, ENDWITH

alternative HTML part, attached files) encoded in


A request for deletion of several messages can be
different ways. How to parse all types of e-mail messages
implemented either with a series of MOVE requests or
should be the task of a separate FoxPro class, but to keep
with a single BMOVE request. All WebDAV verbs for
things simple for this example, the WebmailFolder class
operating with multiple items start with “B”. The BMOVE
parses the message body only partially. It splits the
request has an XML part containing the message IDs (see
message to RAWHEADERS and RAWCONTENT parts
Figure 12).
and then produces a collection of message headers from
the RAWHEADERS.
Other actions
Another “B” verb, BDELETE, is used for purging deleted
Deleting a message
messages from the Trash folder. The top line of the request
Upon deletion, the Hotmail message is moved to the
contains the URI of the Trash folder, and the XML body is
Deleted Items folder. That’s why the WebmailMessage
the list of message IDs. You can find code for that in the
.DeleteMessage method is just a wrapper around the
EmptyTrashFolder method of the WebmailClient class in
MoveMessage method of the same class. The top line of
the download file for this article.
the HTTP request includes the MOVE verb and the URI of
Two other verbs, COPY and BCOPY, can be used for
the message to be moved (see Figure 11). The Destination
copying messages between folders.
header contains the URI for the destination folder.
Also, the Allow-Rename header is included. Note that
Sending a Hotmail message
Microsoft says that even the “hundred per day” limit of
Figure 10. A sent messages imposed some time ago didn’t improve the
sample of the raw situation of abuse by spammers. So, thou shalt not spam!
e-mail message With this in mind, I’ll explain how to send Hotmail
content. messages. If you’re familiar with the Simple Mail Transfer

Figure 11. The HTTP request a Hotmail client sends to the Figure 12. The HTTP request a Hotmail client sends to the
Hotmail server to delete a message from the INBOX folder. Note Hotmail server to delete several messages from the INBOX folder.
that the request has no XML body. The XML body contains message IDs.

12 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


Protocol (SMTP), you may notice that
Table 2. Sending an SMTP message.
Hotmail requires the message to be
formatted very much like SMTP. Element Formatting
However, there are some First comes the MAIL command. MAIL FROM: <sender@msn.com>
differences. On an SMTP server, the RCPT TO: <recipient@somewhere.com>
message has to be sent step by step Then comes the message header. From: <sender@msn.com>
(see Table 2). First, you send the To: <recipient@somewhere.com>
MAIL command and wait for the Subject: test message
Date: Fri, 11 Mar 2005 15:23:00 -0500
“250 Ok” response. Then, you send
MIME-Version: 1.0
the RCPT TO command that also Content-Type: text/plain; charset=us-ascii; format=flowed
requires a “250 Ok” response to Content-Transfer-Encoding: 7bit X-Priority: 3
continue. Then the DATA command X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2900.2527
is sent, and so on.
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2527
Unlike SMTP, the Hotmail server
requires the whole message to be And finally the body of the Test message. Please do not respond.
assembled and sent to the Hotmail message follows.
server in a single POST HTTP
request. Here’s the WebmailClient.SendEmail method: LOCAL href, cMessage, cHeaders

* SendMessage url on Hotmail


PROCEDURE SendEmail href = THIS.GetSendHref()
LPARAMETERS cRecipient, cSubject, cBody, lSaveSent Continues on page 20

Definitions and Additional Resources


Glossary independently to the message destination. The protocol
• HTTP—Stands for HyperText Transfer Protocol (the ensures that there’s no error in transmission and that the
protocol for moving hypertext files across the Internet). entire message arrives.
Requires an HTTP client program on one end, and an HTTP • URI—Stands for Uniform Resource Identifier, the generic
server program on the other. HTTP is the most important term for a coded string that identifies an Internet resource.
protocol used in the World Wide Web. There are currently two practical examples of URIs, namely
• HTTP Request—A message sent from an HTTP client to an Uniform Resource Locators (URLs) and partial URLs.
HTTP server. Includes request line, request header fields, • URL—Stands for Uniform Resource Locator, the global
and optional body section. address of documents and other resources on the World
• HTTP Request Verb (method)—Indicates the method to be Wide Web. The first part of the address indicates what
performed on the resource identified by the Request-URI. protocol to use, and the second specifies the IP address
• HTTP Response—After receiving and interpreting a or the domain name where the resource is located.
request message, a server responds with an HTTP • WebDAV—Stands for Web Distributed Authoring and
response message. Includes status line, response header Versioning, the standard used to save data to a Web site
fields, and optional body section. (as opposed to just reading it from a site).
• MSXML—Microsoft XML Core Services.
• Packet Sniffer—A device or program that monitors the Links
data traveling between computers on a network. • Microsoft Help and Support: You receive an error message
• SMTP—Simple Mail Transfer Protocol, used to send e-mail when you use Outlook Express to access your Hotmail
on the Internet. SMTP is a set of rules regarding the account—http://support.microsoft.com/default.aspx?
interaction between a program sending e-mail and a scid=kb;%5BLN%5D;878462
program receiving e-mail. • WebDAV. RFC 2518—www.ietf.org/rfc/rfc2518.txt
• TCP/IP—Stands for Transmission Control Protocol / • PC World, September 2004. Microsoft Adds New Hotmail
Internet Protocol, the basic communication protocol that’s Fee—www.pcworld.com/news/article/0,aid,117933,00.asp
the foundation of the Internet. All the other protocols, • WebDAV Methods on MSDN—http://msdn.microsoft.com/
such as HTTP, FTP, and Gopher, are built on top of TCP/IP. library/default.asp?url=/library/en-us/e2k3/e2k3/
TCP is typically used by applications that require _webdav_methods.asp
guaranteed delivery. • Microsoft XML Core Services (MSXML)—http://msdn
• TCP Packet—A small package of data. TCP/IP breaks .microsoft.com/library/default.asp?url=/library/en-us/
messages up into packets, and sends each packet xmlsdk/html/xmmscXML.asp

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 13


FoxTalk 2.0

Tips from the VFP Team


9.0
The Microsoft Visual FoxPro Team
This month’s tips from the VFP Team illustrate several of ENDPROC

the new reporting features of VFP 9, which is now available PROCEDURE BeforeBand(nBandObjCode,nFRXRecno)
for purchase. ** Check on Page Footer **
IF nBandObjCode=7
? "Before Page "+TRANSFORM(THIS.PageNo)+ ;
ReportListener events and report debugging " is about to print."
ENDIF
The following code serves as an introduction to how you ENDPROC
can hook your own code into the events that fire in
PROCEDURE AfterBand(nBandObjCode,nFRXRecno)
ReportListener as you progress through a report run. ** Page Footer **
(This program is available in the download file for this IF DEBUGGING AND nBandObjCode=7 AND THIS.PageNo=10
? "Suspending..."
column as IntroListener.prg.) SUSPEND
When you run this code, just choose a report, then ? "Resuming..."
ENDIF
watch the screen and respond to WAIT WINDOWS to ENDPROC
see the contents of the listener’s command clauses
ENDDEFINE
collection, as well as screen output fired for each page in
the BeforeBand method. (Note how it checks the incoming Chaining reports in the VFP 9 report preview
nBandObjCode parameter to determine when we’re on The following code sample demonstrates VFP 9’s ability
the page footer band.) to chain reports in the preview window:
Finally, note that if you change the DEBUGGING
constant in the first line to .T. and then run a report with *This demonstrates how reports can be chained in the
at least 10 pages, the report run will break with a *preview now.

SUSPEND statement at page 10. You can then fire up #define ListenerPrint 0
#define ListenerPreview 1
the debugger and step through the code that’s executing, #define ListenerXML 4
as well as explore some interesting variables and #define ListenerHTML 5
objects. At any time you can RESUME and the report
run will continue. rep1 = GETFILE("FRX", "Pick the first report")
rep2 = GETFILE("FRX", "Pick the second report")
#DEFINE DEBUGGING .F. *See how the range is now respected by the preview:
REPORT FORM (Rep1) RANGE 1,4 ;
CLEAR
OBJECT TYPE ListenerPreview NOPAGEEJECT
LOCAL oListener AS ReportListener
REPORT FORM (Rep2) RANGE 1,4 ;
oListener = NEWOBJECT("MyListener")
OBJECT TYPE ListenerPreview NORESET
oListener.ListenerType=1 && Preview
REPORT FORM (GETFILE("FRX")) OBJECT oListener PREVIEW

DEFINE CLASS myListener AS ReportListener


Tell us about yourself and your development
In early April 2005, we’ll be conducting an online survey
PROCEDURE AfterReport()
ACTIVATE SCREEN
of Visual FoxPro developers to gather more information
? PROGRAM() about the types of software development work you’re
? "***Report is done.***"
ENDPROC doing and the tools that you’re using. This is a great
opportunity to help us better understand your needs as a
PROCEDURE BeforeReport
ACTIVATE SCREEN software developer as we make decisions about future
? "***Report is starting.***" enhancements to Visual FoxPro.
WAIT WINDOW "Let's see REPORT FORM cmd object now."
lnCount=AMEMBERS(laCmds,THIS.CommandClauses) Look for the survey information at http://msdn
FOR i = 1 TO lnCount .com/vfoxpro sometime during the first week of April. ▲
? laCmds[m.i], THIS.CommandClauses.&laCmds[m.i]
ENDFOR
? 504TEAMTIPS.ZIP at www.pinnaclepublishing.com
WAIT WINDOW

Know a clever shortcut? Have an idea for an article for FoxTalk 2.0?
Visit www.pinnaclepublishing.com and click on “Write For Us” to submit your ideas.
14 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com
The Kit Box FoxTalk 2.0

SET Things Right


Andy Kramek and Marcia Akins 6.0 7.0 8.0 9.0

A global solution to a local problem is generally a bad idea. area 1, you’re returned to work area 2 (which is empty)!
But just how bad it can really be? Andy Kramek and Marcia Look at Figure 1 and you’ll see that the value of the
Akins find out when they revisit some old code. The effects of variable lnSelect is showing 2, but the currently selected
some commands like SET EXACT and SET UDFPARMS are well work area (as shown by the datasession window) is
known, but some of the consequences of SET COMPATIBLE actually 1. I can’t find anything wrong and it’s crashing
are more surprising. One, in particular, caused Andy a real the code. This is driving me crazy.
problem recently.
Marcia: Well, that’s a short drive for you.
Andy: I think I’ve found a bug in VFP 9.0, but I really can’t
believe it because it seems so obvious that it could never Andy: Thank you. Now please tell me what’s going on
have slipped through testing. here. If I quit Visual FoxPro and restart, and then do the
exact same thing in the Command window, everything is
Marcia: What are the symptoms? just fine. But when I’m running the app I get what you see
in Figure 1. It’s as if SELECT() is returning the current
Andy: Well, I’m calling one of my standard functions to work area number + 1.
perform a lookup using exact matching. This function
saves the current work area (and some other relevant Marcia: Let’s approach this logically. If you’re getting two
settings) and restores the work area before returning to different behaviors depending on whether you’re running
the calling code. the application or are in the Command window, then it’s a
certainty that something in the application environment is
Marcia: Hang on. Why are you changing work areas causing the problem.
anyway? You can just use the SEEK() function and
reference the alias. Andy: Yes, but what could be doing this? I have no idea
where to start.
Andy: Ah, but the function checks to see whether an index
tag has been specified, and also whether a specified index Marcia: I generally start by looking in the Help file. Let’s
tag actually exists, and if not it uses a LOCATE instead see what it has to say about the SELECT() function, since
of a SEEK(). As you know, you can’t do a “LOCATE IN that’s where you’re seeing the problem. Maybe it’s
<alias>”. But that’s not the problem anyway. Here’s the changed in version 9.0.
relevant part of the code:
Andy: The Help file states that the SELECT() function
LOCAL lnSelect returns either the number of the currently selected work
*** Save current work area
lnSelect = SELECT() area or the highest-numbered unused work area. Passing
<rest of function code here>
*** Restore work area
a parameter of 0 gets the current work area and 1 returns
SELECT (lnSelect)
RETURN
Figure 1. It’s a bug!
SELECT() = 2, but
Marcia: That looks pretty straightforward, so what’s
Work Area = 1.
the problem?

Andy: Well, when this function gets called from inside the
application (it’s an old FP 2.6 app that’s being converted
to run under VFP 9.0), it returns to the wrong work area.

Marcia: What do you mean, it returns to the wrong


work area?

Andy: Exactly what I say. If you call the code from work

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 15


the highest unused work area. highest as stated in the Help file. But I guess it doesn’t
really hurt anything, because it’s still a free work area and
Marcia: It doesn’t say anything about passing no you should never care what the exact number is anyway.
parameters at all, does it? You aren’t actually passing
either the 0 or the 1 in your code. Marcia: It’s interesting that the behavior is identical in
every version since VFP 6.0 (and maybe earlier), and the
Andy: Ah, but 0 is the default. You don’t need to pass it. Help file has said the same thing, too. However, what
really worries me is that this application should even be
Marcia: No, it isn’t. Where does it say that 0 is the default? using the SET COMPATIBLE command. This is one of
All I can find is a reference in the Remarks that says: those commands that really doesn’t have any place in a
production application.
SELECT() returns the number of the current work
area if SET COMPATIBLE is set to OFF. If SET Andy: Why do you say that?
COMPATIBLE is set to ON, SELECT() returns the
number of the unused work area with the highest number. Marcia: Because it’s an example of a “global solution to a
local problem” command.
Andy: Oh dear! I’ll bet that old application has SET
COMPATIBLE = ON somewhere in its startup. Andy: What do you mean by global solution?

Marcia: That would certainly explain what you’re seeing. Marcia: The trouble is that SET COMPATIBLE affects all
What happens if you use SELECT(0) in your function sorts of things—not just the behavior of the SELECT()
instead of just SELECT()? function (which has caused you to waste a couple of
hours trying to find a non-existent bug). There are a
Andy: It works! Checking the settings inside the function, couple of other settings in Visual FoxPro that also
I find that COMPATIBLE is ON, and the Code References illustrate what I mean. For example, when you want to
tool (bless it!) shows that there is indeed a SET do an exact match for a string comparison, you might
COMPATIBLE DB4 in one of the procedures called consider including EXACT = ON in the code before
from the startup program (which is equivalent to SET the comparison. This is a global solution because it
COMPATIBLE ON)! So you’ve solved my problem—it’s affects all string comparisons in the current datasession
not a bug in VFP 9.0 after all. and, unless you restore the prior state, you’ll affect
not only the comparison you’re about to do, but all
Marcia: Glad I could help. Interestingly, there is a bug subsequent operations.
there, but it’s in the Help file and not in the code. Look at
Figure 2. Andy: I see what you mean. Debugging this could be a
nightmare because unless you happen to hit the line of
Andy: Yes, I see. When compatible is ON, the SELECT() code that turns EXACT on, you could run the application
function returns the lowest free work area number, not the for days and never see a problem.

Figure 2. The bug


is in the Help file,
not the code!

16 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


Marcia: Exactly. A much better solution would be to use DO CallMyFunction WITH param1, param2

the == operator to enforce the exact match. This is the


correct local solution to a local problem. then Visual FoxPro treats this as a procedure call and passes
the parameters by reference.
Andy: Well, sort of. If you want to use the SEEK()
Marcia: Right. But it’s actually the setting of UDFPARMS
function, you can’t use ==. In fact, that’s precisely
that defines how the parameters are passed when you
why I created my own ExactSeek() function—the
call a block of code as a function. It’s just that the default
code sets EXACT on for the SEEK() and then restores
happens to be set as TO VALUE. If you change the setting
it to its prior setting before returning control to the
of UDFPARMS, then even though you call the code as a
calling program.
function, the parameters get passed by reference (see
Marcia: Yes, but what are you actually doing with this Figure 3).
code? You’re localizing the solution by ensuring that the
setting is used only for the execution of a single, self- Andy: Ouch! That could break a lot of code and would be
contained block of code. Perhaps another example will really nasty to debug. Why would anyone do that? If you
help. How about SET UDFPARMS? need to pass parameters by reference (as for an array),
you can do so simply by prefixing the parameter name
Andy: Remind me. I’m not even sure what that setting with @. In fact, I didn’t even know there was another way
is for. to do it.

Marcia: Parameters can be passed either by reference or Marcia: Now do you see what I mean by global solutions
by value. When a parameter is passed to a function or to local problems?
procedure by reference, any changes made to its value in
the called code are reflected in the original value in the Andy: Definitely! But we’ve drifted off the point a bit.
calling program. Conversely, when a parameter is passed Let’s get back to SET COMPATIBLE. What else does that
by value, the called code can change that value but the affect besides the SELECT() function?
value in the calling program remains unchanged.
Marcia: The Help file lists 32 entries (including one that
Andy: Yes, and I also know that Visual FoxPro interprets merely says “Menu commands” but doesn’t actually tell
the code being called by the mechanism by which us which ones). However, only about a dozen of them
parameters are passed. So, when the calling syntax looks seem immediately applicable to Visual FoxPro—the
like this: others are really concerned with FoxPro 2.x issues. But
the ones that are relevant include some really nasty
luRetVal = CallMyFunction( param1, param2 ) behavior changes.

Visual FoxPro treats this as a function call and passes the Andy: Oddly, there are a lot of commands mentioned
parameters by value. However, if the same code is called there that, in the Help file, make no reference to SET
like this: COMPATIBLE at all.

Figure 3. The effect


of SET UDFPARMS.

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 17


Marcia: Like? Marcia: In Visual FoxPro, redimensioning an existing
array adds additional rows and columns but doesn’t affect
Andy: APPEND MEMO, BROWSE, INKEY(), LASTKEY(), the existing contents, right?
SET MESSAGE, and SET PRINTER, to name half a
dozen. In my (admittedly simplistic) tests I can’t actually Andy: Sure. Also, I usually initialize my arrays to either
find any differences with any of them. I wonder why empty strings or to zero to avoid issues with the default
they’re mentioned? behavior, which sets all elements to .F. That’s easy using
the STORE command.
Marcia: Let’s deal with the ones that we know about for
sure first. The one that really bothers me the most is the Marcia: But both of these behaviors depend on the
behavior of the DIMENSION and STORE commands setting of SET COMPATIBLE. Look at Figure 4 and
when dealing with arrays. Figure 5. With COMPATIBLE = ON, not only does
redimensioning an array reinitialize it (whether you use
Andy: I’ll bite. What’s the big deal? DIMENSION or DECLARE makes no difference here),
but using STORE deletes the array
entirely and creates a single variable
with the same name.

Andy: Oh, that’s bad! I’ve been using


arrays extensively in this rewrite,
and I confess that I haven’t noticed
any odd behavior yet. But this is
something I really do have to check
on—that behavior is just absurd.

Marcia: But presumably that’s how


dBase was designed. After all, that is
what compatibility mode is doing—
making VFP behave like dBase.

Andy: I was just looking at FSIZE().


That’s another horror. Instead of
returning the size of the specified
Figure 4. SET COMPATIBLE affects array resizing. field, it tries to return the size of a file
with the name specified. The general
result is that you get a “File is not
found” error.

Marcia: Exactly, and here’s another


one that will catch you out if you’re
not careful. I know that in the data
you’re working on now there are
character keys that are padded
with spaces.

Andy: Yes, not my choice, I have


to say.

Marcia: Does the Fox 2.x code use


the LIKE() function anywhere,
by chance?

Andy: Yes, it does, as a matter of fact.


Figure 5. SET COMPATIBLE also changes how STORE affects an array. The first time I saw that function I

18 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com


had to go and look it up because I didn’t know what it clearly there were some pretty fundamental differences.
did. Why do you ask that?
Marcia: Yet again it proves that our assumptions are
Marcia: Well, now we know why, or at least can make a often flawed. The rule, as always, when confronted
good guess at why, it uses COMPATIBLE = ON. Under with unexpected behavior is to check things out coolly
this setting, strings are trimmed before being compared and logically instead of immediately crying “bug.” And,
by LIKE() as opposed to the normal Visual FoxPro of course, when all else fails, you can always read the
behavior where trailing spaces are considered part of fine manual.
the values being compared.
Andy: Can’t argue with that! All of the code we used to
Andy: Aha! That explains something else I’d noticed but illustrate the differences in behavior is included in the
not really understood at the time. The original code used download for this month’s column. ▲
LIKE() inside a scan loop to select matching records, but
when I rewrote the code to use a SQL query I got no
504KITBOX.ZIP at www.pinnaclepublishing.com
results until I explicitly trimmed the key fields on which
I was joining the tables. I hadn’t realized that it was
Andy Kramek is a long-time FoxPro developer, FoxPro MVP, independent
only the setting of COMPATIBLE that made the original
consultant, and joint owner of Tightline Computers Inc., based in
code work.
Akron, OH. A veteran conference speaker, he has published widely
and can be found online in the CompuServe forums (http://
Marcia: Insidious, isn’t it? These are exactly the
go.compuserve.com/msdevapps) and Foxite (www.foxite.com).
problems you can encounter when using these global andykr@tightlinecomputers.com.
settings. Although solving one problem, they often cause
unexpected problems in other, totally unrelated areas. Marcia Akins is a FoxPro MVP, independent consultant, and joint
owner of Tightline Computers Inc., based in Akron, OH. A veteran
Andy: What surprises me is the extent of the differences conference speaker, she has published widely and is well known for her
in behavior when they do arise. I’d always assumed contributions to Tek-Tips (www.tek-tips.com) and the Universal Thread
that FoxPro and dBase were pretty much identical, but (www.Universalthread.com). marcia@tightlinecomputers.com.

Know a clever shortcut? Have an idea for an article for FoxTalk 2.0?
Visit www.pinnaclepublishing.com and click on “Write For Us” to submit your ideas.

Don’t miss another issue! Subscribe now and save!


Subscribe to FoxTalk 2.0 today and receive a special one-year introductory rate:
Just $129* for 12 issues (that’s $30 off the regular rate)

NAME ❑ Check enclosed (payable to Pinnacle Publishing)


❑ Purchase order (in U.S. and Canada only); mail or fax copy
COMPANY
❑ Bill me later
❑ Credit card: __ VISA __MasterCard __American Express
ADDRESS
CARD NUMBER EXP. DATE

CITY STATE/PROVINCE ZIP/POSTAL CODE


SIGNATURE (REQUIRED FOR CARD ORDERS)

COUNTRY IF OTHER THAN U.S.


Detach and return to:
Pinnacle Publishing ▲ 316 N. Michigan Ave. ▲ Chicago, IL 60601
E-MAIL Or fax to 312-960-4106

* Outside the U.S. add $30. Orders payable in


INS5
PHONE (IN CASE WE HAVE A QUESTION ABOUT YOUR ORDER) U.S. funds drawn on a U.S. or Canadian bank.

Pinnacle, A Division of Lawrence Ragan Communications, Inc. ▲ 800-493-4867 x.4209 or 312-960-4100 ▲ Fax 312-960-4106

www.pinnaclepublishing.com FoxTalk 2.0 Sample Issue 19


Accessing Hotmail and MSN... simplified in this example and produces only plain text
messages with no attachments.
Continued from page 13

* assembling the message Summary


cMessage = 'MAIL FROM:<' +; By watching a packet sniffer, you can determine the
THIS.email + '>' + CRLF +;
'RCPT TO:<' + cRecipient + '>' + CRLF +; protocols and messages required by other HTTP-based
'' + CRLF +;
'From: <' + THIS.email + '>' + CRLF +; applications and then mimic those messages with
'To: <' + m.cRecipient + '>' + CRLF +; standard Microsoft HTTP and XML objects. Not only
'Subject: ' + m.cSubject + CRLF +;
'Date: ' + TTOC(DATETIME()) + CRLF +; can you use this capability to access your Hotmail
'MIME-Version: 1.0' + CRLF +; account (although it may have to be a paid account),
'Content-Type: text/plain; ' +;
'charset="Windows-1252"' + CRLF +; but you can use the same principles to further Internet-
'X-Mailer: ' + VERSION() + CRLF +;
'' + CRLF +; enhance your applications. ▲
cBody

* additional HTTP headers 504ANATOLIY.ZIP at www.pinnaclepublishing.com


cHeaders = "Content-Type: message/rfc821;" +;
"SAVEINSENT: " + IIF(m.lSaveSent, "t","f")
Anatoliy Mogylevets is an independent consultant for Visual FoxPro
* sending the message
THIS.SendHttpRequest("POST", m.href,; and Web development in Scarborough, Ontario, Canada. He has
m.cHeaders, m.cMessage) been developing FoxPro applications since 1990. He’s also the owner
of the Web site “Using Win32 (WinAPI) Functions in Visual FoxPro” at
Note that the message assembling routine is www.news2news.com/vfp. devicecontext@msn.com.

Sample Issue Downloads


• 504HENNIG.ZIP—Source code to accompany Doug Hennig’s • 504TEAMTIPS.ZIP—Source code to accompany this
article, “I Got Rendered Where?” month’s “Tips from the VFP Team” column.

• 504ANATOLIY.ZIP—Source code to accompany Anatoliy • 504KITBOX.ZIP—Source code to accompany Andy


Mogylevets’ article, “Accessing Hotmail and MSN Accounts in Kramek and Marcia Akins’ article, “The Kit Box: SET
Visual FoxPro.” Things Right.”

For access to current and archive content and source code, log in at www.pinnaclepublishing.com.

Editor: David Stevenson (david@topstrategies.com) FoxTalk 2.0 (ISSN 1042-6302)


CEO & Publisher: Mark Ragan is published monthly (12 times per year) by:
Group Publisher: Michael King
Pinnacle Publishing
Executive Editor: Farion Grove A Division of Lawrence Ragan Communications, Inc.
316 N. Michigan Ave., Suite 300
Questions? Chicago, IL 60601

Customer Service: POSTMASTER: Send address changes to Lawrence Ragan Communications, Inc., 316 N. Michigan
Ave., Suite 300, Chicago, IL 60601.
Phone: 800-493-4867 x.4209 or 312-960-4100
Fax: 312-960-4106 Copyright © 2005 by Lawrence Ragan Communications, Inc. All rights reserved. No part of this
periodical may be used or reproduced in any fashion whatsoever (except in the case of brief
Email: PinPub@Ragan.com quotations embodied in critical articles and reviews) without the prior written consent of
Lawrence Ragan Communications, Inc. Printed in the United States of America.
Advertising: RogerS@Ragan.com
Brand and product names are trademarks or registered trademarks of their respective
Editorial: FarionG@Ragan.com holders. Microsoft is a registered trademark of Microsoft Corporation. The Fox Head logo,
FoxBASE+, FoxPro, and Visual FoxPro are registered trademarks of Microsoft Corporation.
FoxTalk 2.0 is an independent publication not affiliated with Microsoft Corporation. Microsoft
Pinnacle Web Site: www.pinnaclepublishing.com Corporation is not responsible in any way for the editorial policy or other contents of
the publication.

Subscription rates This publication is intended as a general guide. It covers a highly technical and complex
subject and should not be used for making decisions concerning specific products or
applications. This publication is sold as is, without warranty of any kind, either express or
United States: One year (12 issues): $159; two years (24 issues): $278 implied, respecting the contents of this publication, including but not limited to implied
Other:* One year: $189; two years: $338 warranties for the publication, performance, quality, merchantability, or fitness for any particular
purpose. Lawrence Ragan Communications, Inc., shall not be liable to the purchaser or any
Single issue rate: other person or entity with respect to any liability, loss, or damage caused or alleged to
be caused directly or indirectly by this publication. Articles published in FoxTalk 2.0 reflect
$20 ($25 outside the United States)* the views of their authors; they may or may not reflect the view of Lawrence Ragan
Communications, Inc. Inclusion of advertising inserts does not constitute an endorsement
* Funds must be in U.S. currency. by Lawrence Ragan Communications, Inc., or FoxTalk 2.0.

20 FoxTalk 2.0 Sample Issue www.pinnaclepublishing.com

Potrebbero piacerti anche