FoxPro White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |  
 
 

Using the Windows Shell API and Internet Explorer Controls in Visual FoxPro
Desktop applications

 

By Rick Strahl

www.west-wind.com
 

Last Update:

August 12, 2008

 

 

Source Code for this article:

http://www.west-wind.com/presentations/shellapi/shellapi.zip

 

Source Code for DevCon WebBrowser Session:

http://www.west-wind.com/presentations/shellapi/WebBrowser_Session.zip

 

 

 

Internet enabling applications doesn’t have to be difficult or involve a complete rewrite of applications. Instead you can use some of the tools built into the Windows Shell for quickly integrating Internet and Internet related content easily into your own applications. In this article Rick shows several ways that you can utilize the Windows Shell API by:

 

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 


When people think about Internet development the first thing that comes to mind is rebuilding an application as a dynamic Web application with an HTML front end. But there’s lots of life left in desktop applications. Extending these applications with Internet content provides enormous benefits in enhancing and enabling existing applications with new features that don’t take much to implement.

 

In this article I’ll describe a few features of the built in Shell interfaces that Windows provides out of the box – mostly through Internet Explorer and with ways that you can use to integrate this functionality into your own applications with very little code. Most of the concepts and samples are very short and can be integrated with a few lines and yet provide big benefits for existing applications without much fuss.

The ShellExecute API

Although the Shell API is nothing new, it's one of those useful features of Windows that you can always find new uses for. The Shell API is essentially an extension of the Windows Shell that provides most of the same functionality you have to launch applications in Windows Explorer through a programmatic interface which includes:

 

  • Plain executable files like .exe, .bat, .vbs etc.
  • Any file on the local system or network with a registered extension (.doc, .pdf, .txt etc.)
  • Web URLs like http://www.west-wind.com/
  • FTP Urls (ftp://ftp.west-wind.com/)
  • Email clients using the mailto: directive
  • Any other ‘monikers’ that Windows is familiar with

 

What makes the Shell API handy is that it looks at the file or moniker (which are ‘addresses’ that are prefixed by a protocol prefix such as http:, ftp:, mailto: etc.)  you pass, and then ‘runs’ it automatically by launching the application that is associated with it. For example, if you pass a Web URL it will automatically launch Internet Explorer or the default browser. If you pass a Word document Word is loaded, or if it’s not installed Word Pad.

 

If you aren’t familiar with the Shell API, the easiest way to access it is through a simple API call to the ShellExecute function which is declared like this:

 

DECLARE INTEGER ShellExecute ;

    IN SHELL32.dll ;

    INTEGER nWinHandle,;

    STRING cOperation,;

    STRING cFileName,;

    STRING cParameters,;

    STRING cWorkingDirectory,;

    INTEGER nShowWindow

 

This function expects a handful of simple parameters that you can pass easily from VFP. Typically the WinHandle (Window Handle) can be passed as 0 which means the launched application is launched on the Windows Desktop. Alternately you can use the FindWindow API (as shown in Listing 1 to pass the active VFP window) to pass the active VFP application window. The Operation is used to use any of the associated word with the extension. For example a Word document allows, “Open” and “Print” as options.  You can find what verbs are supported in the registry by looking up the document extension. Filenames should always be specified as full paths, and Parameters specifies any optional parameters. If you don’t pass any parameters the filename itself is used as a parameter (for example, SomeFile.doc becomes the parameter that is passed to Word). The Parameters option is mainly for real executable files that require a command line. The WorkingDirectory specifies were the program runs. Finally ShowWindow lets you specifies the WindowMode as to how the application is launched which should be left as 1 usually – active and topmost, although this is only respected if the application isn’t already running. Most applications reuse windows for launching new content so when you use ShellExecute for multiple calls to URLs they all go to the same browser instance instead of into a new window each. However, this behavior is determined by the host application.

 

To make life easier and address the most common scenario of displaying content especially in a browser I always create a wrapper function named GoUrl() shown in Listing 1.

Listing 1 – Creating a wrapper around ShellExecute for easier access

FUNCTION GoUrl(tcUrl, tcAction, tcDirectory, tcParms)

 

IF EMPTY(tcUrl)

   RETURN -1

ENDIF

IF EMPTY(tcAction)

   tcAction = "OPEN"

ENDIF

IF EMPTY(tcDirectory)

   tcDirectory = SYS(2023)

ENDIF

IF EMPTY(tcParms)

   tcParms = ""

ENDIF

 

DECLARE INTEGER ShellExecute ;

    IN SHELL32.dll ;

    INTEGER nWinHandle,;

    STRING cOperation,;

    STRING cFileName,;

    STRING cParameters,;

    STRING cDirectory,;

    INTEGER nShowWindow

 

DECLARE INTEGER FindWindow ;

   IN WIN32API STRING cNull,STRING cWinName

 

RETURN ShellExecute(FindWindow(0,_SCREEN.caption),;

                    tcAction,tcUrl,;

                    tcParms, tcDirectory,1)

 

The code for the sample is found in the ShellExec.prg file included in the source from this article. To load this function and the others I'll shortly describe just DO ShellExec.

Accessing Web Content

Let’s start by accessing some Web content. To open a Web Url try:

DO ShellExec

GoUrl("http://www.west-wind.com/")

 

This is quite useful if you have applications of your own and you want people to go to your Web site for certain operations. Alternately you can link to something a little more precise on a Web site. Check out Figure 1 which shows the About form of a shareware application. On this page it might be useful to link to registration links directly from the page with a dynamic URL like this:

 

GoUrl("http://www.west-wind.com/wwHelp/" + ;

      "wwHelpInfo.asp#Pricing")

GoUrl("http://www.west-wind.com/wwStore/" + ;

      "item.wws?sku=wwhelp30")

 

The latter Url takes the user directly to the registration page in the dynamic company Web site from which they can place the order.

 

Figure 1 – Using ShellAPI links from the buttons brings up relevant Urls on the West Wind Web site. Here the pops up the browser to register a shareware product online. You can link back to information, sales and support links directly with a single line of code and attach it to anything: Buttons, menu items, labels…

 

You can call this code from anywhere in VFP: Buttons, menu options, labels, images – unlike the Hyperlink control that ships with VFP since version 6.0. Although the Hyperlink control also provides this functionality I’ve always disliked it because it works only through Internet Explorer and always wants to open everything in new browser windows which becomes unmanageable quickly if users frequently click on links. It also always uses IE even if the user’s default browser is another browser like Mozilla or Netscape. ShellExecute always respects the system settings.

 

For something a little more generic try adding some search functionality to your application by linking directly to the Google search engine with a search phrase:

 

lcSearchString = "FoxPro Tools"

GoUrl("http://www.google.com/search?hl=en&ie=UTF-8&oe=UTF-8&q=" + lcSearchString )

 

And off you go directly to a Google search with the specified search parameters. In fact, Google lets you even limit searching to a specific site, like your own company's site for example. If I want it to search only the West Wind site I can use:

 

lcSearchString = "Html Help site:west-wind.com"

GoUrl("http://www.google.com/search?hl=en&ie=UTF-8&oe=UTF-8&q=" + lcSearchString )

 

If you do this a lot, you’ll probably want to create a small wrapper function like this:

 

FUNCTION SearchGoogle(lcSearch, lcSite)
IF EMPTY(lcSite)
  lcSite = ""

ELSE

  lcSite = " " + lcSite
ENDIF

RETURN GoUrl("http://www.google.com/search?hl=en&"+;

             "ie=UTF-8&oe=UTF-8&q=" + ;

             lcSearch + lcSite)

 

Here’s another useful example. Do you have addresses in your applications and do you frequently need to show a map to the location in question? How about linking to one of the map sites and automatically displaying the map content when right clicking on the Address field? Listing 2 goes to the MapQuest site and displays a map for the location specified.

 

Listing 2 – Displaying a map for an address with MapQuest

***********************************************************

FUNCTION ShowMap(lcStreet,lcCity,lcState,lcZip,lcCountry)

***********************************************************

LPARAMETERS IF EMPTY(lcStreet)

  lcStreet = ""

ENDIF

IF EMPTY(lcState)

   lcState = ""

ENDIF

IF EMPTY(lcZip)

   lcZip = ""

ENDIF

IF EMPTY(lcCountry)

   lcCountry = "US"

ENDIF

IF EMPTY(lcCity)

  lcCity=""

ENDIF

 

GoUrl("http://www.mapquest.com/maps/map.adp?country=" + ;

       lcCountry + "&addtohistory=&address=" + ;

       UrlEncode(lcStreet) + ;

       "&city=" + UrlEncode(lcCity) + "&state=" + ;

       UrlEncode(lcState) +  "&zipcode=" + ;

       UrlEncode(lcZip) + "&homesubmit=Get+Map&size=big")

 

I’ve used this in several applications where the address is then handled with a right click menu or double-click event where the operation performed is just a couple of lines:

 

ShowMap( "32 Kaiea Place","Paia","HI",;

         "96779","US")

 

Note that the CountryID is a two letter ISO country code like US, CA, DE etc. This pops up a map as shown in Figure 2.

 

Figure 2 – Popping up a dynamic map as easy as passing the right URL Parameters to ShellExecute() or calling the ShowMap() wrapper function.

 

The mapping function requires another function called Urlencode which is also included in the ShellExec.prg source. It’s necessary for addresses to be encoded in URL format, which is especially important if you’re using international addresses. Without this some lookups may fail due to misinterpretation of the data. URLEncoding replaces spaces with + signs and replaces extended characters (other than A-Z and digits to hex values like %0A (for a carriage return). We’ll reuse this function later in a few other places as well.

 

Listing 3 – A function to URL encode URLs and HTTP POST content

Function UrlEncode( tcValue, llNoPlus )

LOCAL lcResult, lcChar, lnSize, lnX

 

*** Do it in VFP Code

lcResult=""

 

FOR lnX=1 to len(tcValue)

   lcChar = SUBSTR(tcValue,lnX,1)

   IF ATC(lcChar,"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") > 0

      lcResult=lcResult + lcChar

      LOOP

   ENDIF

   IF lcChar=" " AND !llNoPlus

      lcResult = lcResult + "+"

      LOOP

   ENDIF

   *** Convert others to Hex equivalents

   lcResult = lcResult + "%" + RIGHT(transform(ASC(lcChar),"@0"),2)

ENDFOR

 

RETURN lcResult

 

This mechanism can be easily applied to any number of applications and content on the Web. Although this type of content integration is pretty high level and doesn’t automate everything about the Web access it gets a lot of mileage out of very little effort. It’s especially useful if you can integrate with content that exists on your own Web site to aid customers.

 

Keep in mind that if you use this sort of interface to a Web page in your own applications you are potentially at the mercy of the provider of this Web page. If MapQuest or Google decide to change their URL structure it's possible that lookups stop working all of a sudden. For large sites like Google or MapQuest this is not very likely – they actually don't mind if you link to them since when you do, you still end up with the advertisements that pay their bills. However, if you deal with smaller Web sites, they might not even consider their pages being used as an 'API' through external applications and change frequently.

 

You have to weigh the importance of the functionality yourself. I tend use functionality like this for convenience features that are useful and add value, but aren't necessarily mission critical. If they are critical it's wise to check with the provider and see if you can work out some guarantee of service deal.

FTP access

You can also use the ShellAPI to access FTP. As you probably know Explorer/Internet Explorer can directly handle FTP access from within the shell and you can take advantage of this functionality simply by pointing ShellExecute at an FTP Url like this:

 

GoUrl("ftp://www.west-wind.com/")

 

And voila, you now have access to a basic FTP client that can upload and download data with via drag and drop into and out of Explorer. Figure 3 shows an example of Explorer brought up through ShellExecute in FTP mode.

 

Figure 3 – Integrating Explorer's FTP access is as easy as popping open an FTP link through ShellExecute. Here ftp://www.west-wind.com/ takes you to the West Wind FTP site where you can use Explorer's drag and drop file moving to up and download from the FTP site.

 

Note that you can also force a login to occur automatically via the ftp: moniker:

 

GoUrl("ftp://ricks:password@www.west-wind.com/uploads")

 

So if a login is required your application could optionally ask for or store a username and password and then pass this on to the FTP link. This is obviously not a fully automated solution but if your application requires that users upload or download a number of files having a full featured FTP client is often useful as opposed to having the application provide the UI for the FTP transfer itself.

Document Access

As mentioned earlier, ShellExecute can also 'run' documents by identifying extensions of file requests and mapping the extension to the application that it belongs to. So you can do things like:

 

GoUrl("d:\articles\fpa\shellexecute.doc")

GoUrl("d:\articles\fpa\shellexecute_SourceCode.zip")

GoUrl("d:\articles\fpa\shellExecute.pdf")

GoUrl("d:\Articles\fpa\ShellExecute.htm")

GoUrl("d:\Articles\fpa\ShellExecute_Data.xml")

 

Each of these files is brought up in it's native viewer which is Word, WinZip, Adobe PDF Reader and IE in both of the last cases respectively. This is useful when your application needs to manipulate files of various types and let the user interact with it. But keep in mind that all you can do here is basically bring up a document and view it or in a few instances of document types that support it do things like Print.

 

The nice thing about this is that the document comes up in its native environment so you can continue to use that environment to perform common tasks. For example, you might pop up a Word document and let the user modify the document and save it using Word. The application at a later point might pick up the change and then further act on that document such as sending it to a Web site, or updating a database with the data.

Displaying text as Text, HTML and XML

Here's a simple example of how I use this functionality in my own development. I often generate large strings in my code such as HTML or XML that is either generated or downloaded from a Web site. This textual data is usually stored inside of string which can get quite large. It's often difficult to see what's stored in these strings inside of VFP, so it's nice to display the data in a way that you can look at it as a whole. It's quite useful to look at what was generated both in rendered HTML format as well as text. To do this I use a simple function shown in Listing 4 which renders the HTML string passed to it into the configured Web Browser for the system.

 

Listing 4 – Displaying a text strings inside of a text editor with the ShowText function.

FUNCTION ShowHtml(lcHTML, lcFile)

 

lcHTML=IIF(EMPTY(lcHTML),"",lcHTML)

lcFile=IIF(EMPTY(lcFile),;

           SYS(2023)+ "\ww_HTMLView.htm",lcFile)

 

STRTOFILE(lcHTML,lcFile)

 

RETURN GoUrl(lcFile)

 

All this function does is take an HTML input string and copies it to a file, then uses ShellExecute() via the GoUrl() function I showed earlier and displays this rendered HTML in the browser. You can optionally pass a filename to copy the file to, which is useful if you are depending on relative files, such as images or stylesheets that may be referenced within the HTML document. If you generate into the default TEMP directory those files are not likely to be found and cause broken links to images. This is a great debugging tool when generating HTML so that you can quickly see what you're generating.

 

I use this functionality in many places. For example, my inventory management application for my Web site lets me edit item descriptions in HTML format and the user gets the option to preview the output of the text he's written simply by passing the contents of the textbox to ShowHtml(). Easy and very useful as an HTML preview.

 

You can also display Text and XML strings in this fashion. Listing 5 shows the ShowText() and ShowXML() functions which in turn call ShowHtml with predefined filename extensions that bring up either the configured text editor (NotePad by default or in my case TextPad) or Internet Explorer in its XML view.

 

Listing 5 – Displaying text and XML strings quickly with ShowText and ShowXML

FUNCTION ShowText(lcText, lcFile)

IF EMPTY(lcFile)

  lcFile=IIF(EMPTY(lcFile),SYS(2023)+"\ww_HTMLView.txt",lcFile)

ENDIF

 

RETURN ShowHTML(lcText,lcFile)

 

FUNCTION ShowXML(lcXML, lcFile)

 IF EMPTY(lcFile)

  lcFile=IIF(EMPTY(lcFile),SYS(2023)+"\ww_HTMLView.xml",lcFile)

ENDIF

 

RETURN ShowHTML(lcXML,lcFile)

 

I find these function indispensable during debugging of applications especially distributed and Web applications where I often deal with large amounts of HTML and XML as shown in Figure 4. Because strings generated are often difficult to display inside of VFP it's nice to be able to quickly view what a variable contains or whether the output generated renders correctly in HTML. With XML viewing in IE is a quick way to see if the XML document is valid.

 

Figure 4 – Displaying large strings of HTML and text in an external editor is often the best way to see what the strings contain while debugging or demonstrating code. Here ShowHtml() was called from the code and ShowText() from the command window while sitting on a breakpoint.

 

Driving the Email client

You can also use the Shell API to allow users to send email messages through their configured email client. To bring up an email client you need nothing more than this:

 

GoUrl("mailto:rstrahl@west-wind.com")

 

This pops up the email client with an empty message and subject area. You can even force a little more information to the client by providing additional information via 'querystring' parameters such as this:

 

GoUrl("mailto:rstrahl@west-wind.com?subject=Surf's up," + ;

      " Rick&Body=Drop everything, it's going big!")

 

To make this more generic you might want to use the function shown in Listing 6.

Listing  6 – Displaying the configured email client with subject and message body is easy

FUNCTION ShowEmail( lcRecipient,lcSubject,lcBody )

RETURN GoUrl("mailto:" + lcRecipient + ;

             "&Subject=" + STRTRAN(UrlEncode(lcSubject),"+"," ") +;

             "&Body=" + STRTRAN(UrlEncode(lcBody),"+"," "))

 

Note that UrlEncoding the content is required to handle extended characters, specifically carriage returns, which are dropped if you don't use UrlEncode(). I use this functionality in a number of applications for sending out email confirmations from within my applications.

 

Listing 7 shows a snippet from a Point of Sale application where credit cards are validated. If the credit card validation fails the customer needs to be notified via email that the card failed and that he needs to enter a new card or check with his CC provider. In this case the message to be sent is a text merge document stored on disk into which information of the invoice business object is merged. For example, <<poInv.InvNo>> holds the invoice number, <<poInv.InvTotal>> holds the total and <<poCust.FirstName>> holds the first name which is used in the greeting. The result of this merge and call to ShowMail() is shown in Figure 5.

 

Listing 7 – Sending a custom email by displaying an email in the default email client

poInv = THISFORM.oInv

poInv = poInv.oData

poCust = poCust.oData

 

lcEmail = TRIM(poCust.Email)

 

*** Merge the declined order message into the template

lcMessage = TextMerge( FILETOSTR("DeclinedOrder.wc") )

 

*** display the email message

ShowEmail(lcEmail, "Re: West Wind Technologies " + ;

                   "Order Confirmation - Declined. #" + ;

                   TRIM(poInv.Invno), lcMessage)

 

 

Figure 5 – By using the mailto: protocol it's possible to pre-seed an entire email message with ShellExecute so all you have to do is press the Send key. Alternately you can further edit the message before sending – a common scenario with messages.

 

For standalone applications this type of Email automation is often sufficient and actually preferred over using a fully automated email interface such as MAPI or an SMTP client. The advantage here is that you can rely on the user's email settings rather than explicitly configuring an email connection. In addition, if you need to display a message for review or editing, why reinvent the wheel? Your email client already provides a familiar interface to the user and your message can be tracked in the Sent Items folder like regular respondence. Note that this feature does not require MAPI – all you need is an installed mail client that supports this protocol (Outlook and Outlook Express and most other Web browser based email clients  do).

ShellExecute summary

ShellExecute provides a great way to easily link to all sorts of content easily from your applications. The interface is flexible enough so that you can often pass additional information to finely tune the information that pops directly to the context of your document application. It's easy to use and is a totally non-intrusive way to integrate new functionality into your existing applications. But it's important to understand that this is a 'Show and Forget' type of technology that's meant for interactive use. You can display content but once displayed your application looses the connection with the displayed content and can't be further automated directly.

 

If you need to more control after the document as been opened, you'll need more sophisticated tools like Automation (like Office Automation, Internet Explorer Automation, MAPI etc.). In the next section I'll show you how to automate some of the functionality we've discussed here by using Automation with Internet Explorer and then using the Web Browser control to more closely integrate the content with your own application.

Internet Explorer Automation

Internet Explorer is really the core of the Windows Shell in and through it you can expose all of the various documents and monikers I've shown with ShellExecute. Internet Explorer exposes Automation interfaces in a couple of different yet similar ways:

 

  • The InternetExplorer.Application Automation Object
  • The Microsoft Web Browser Control

 

Like ShellExectute() you can use both mechanisms to display a variety of content, but the content always loads through the Web Browser, not in in the specified application. This is especially useful for HTML based content, which can be fully automated through the rich object model exposed by these components.

 

Both provide more or less the same functionality, exposing most of Internet Explorer's capabilities to your applications for programmatically controlling the content displayed. The Automation object works best if you want to display the content externally to your application in a separate window, while the Web Browser control works best for embedding content directly into your own forms. Although the latter may seem a strange proposition at first, I'll show you some examples that make good use of HTML based content generated from local applications as well as displaying content retrieved from the Internet.

 

The other important feature that both controls provide is the ability to manipulate HTML content within the components which means you can automate many HTML based tasks and drive Web content from within your applications. The rich object model allows you for accessing each individual element, modifying it and even the ability to visually edit the content inside of an HTML document. It's a very large object model to get familiar with, but it also very powerful and allows for many advanced concepts that make it possible to share data between the Web and desktop application interfaces.

COM Automation 101

Let's start with the InternetExplorer.Application Automation object. Here's the basic syntax you need to do to open a URL in IE:

 

oIE = CREATEOBJECT("InternetExplorer.Application")

oIE.Visible = .t.
oIE.Navigate("http://www.west-wind.com/")

… do whatever you need to until you're done with it

oIE.Visible = .f.

oIE = null

 

This pops up a new instance of Internet Explorer and then displays the Web site that I specify in the Navigate method. The instance of Internet Explorer sticks around as long as you have a reference to it. Note that you have to set Visible to .F. to completely release the instance of Internet Explorer. If you don't set Visible to .F. your reference goes away but the IE window remains open and active, but now disconnected from your application.

 

As before you can also load non-Web content through this interface so the following navigations also work as you would expect:

 

oIE.Navigate("d:\articles\fpa\IE.doc")

oIE.Navigate("d:\articles\fpa\IE.pdf")

oIE.Navigate("ftp://www.west-wind.com/")

 

Unlike the ShellAPI calls though any supported application will open inside of Internet Explorer, so a Word document will show as an ActiveDoc inside of the IE shell as shown in Figure 6. As you can see this causes the Word Interface to be merged with the Internet Explorer interface and menus. In this case with a local file you can edit the document and save it right back to the original file.

 

Figure 6 – Loading a Word document through Internet Explorer causes the Word document to display as an ActiveDoc inside of the IE shell.

 

In these examples, I used local disk content, but you can also retrieve this same type of content from the Web. So the following would produce the same results:

 

oIE.Navigate("http://www.west-wind.com/files/ShellInterfaces.doc")

 

It's important to understand that when you bring up files from a URL (http://www.west-wind.com/somefile.doc) as opposed to a local file (d:\temp\somefile.doc), you can still edit the text, but you cannot save it back to its original location on the Web. When you use the Save As… option in Word for example, your save locations can only go to the local or network drives.

         

If you load a document type that can't be viewed directly through the shell such as a Zip file for example you get a download dialog as shown in Figure 7. To do this you'd use:

 

oIE.Navigate("http://www.west-wind.com/files/wwIPStuff.zip")

 

Figure 7 – Accessing a URL that loads a 'document' or file that can't be directly opened through Internet Explorer gives a download dialog just like you would get if you clicked on a hyperlink from a Web page.

 

From here the user can either open the zip file directly in their Zip editor configured or save the file to disk. This save operation cannot be automated through code and requires manual user intervention.

Basic Automation with Web Content

At this point we've really seen little that is different from what that ShellExecute provides with the exception that you can control the lifetime of the browser window. However, the InternetExplorer.Application object includes a rich object model that allows you to interact with the HTML document loaded into a browser instance. It also provides you an Interface for capturing a rich set of events. This is true both for the Application object as well as the Web browser control.

 

To demonstrate the basics of document access try the following from the command window:

 

oIE = CREATEOBJECT("InternetExplorer.Application")

oIE.Visible = .t.

oIE.Height = 600

oIE.Width = 800

oIE.ToolBar = .F.

oIE.AddressBar= .F.

 

oIE.Navigate("http://www.west-wind.com/")

 

*** Retrieve the entire HTML document as a string

lcHtml = oIE.Document.DocumentElement.OuterHtml

 

If you'd rather retrieve just the Body (inside of the <body></body> tags) of the HTML document you can use:

 

lcHtml = oIE.Document.Body.OuterHtml

 

This will work fine from the Command Window but if you run this code in a program you'll likely run into an error that says that the document object was not found. You have to be careful in application code because the HTML document loads in the background on another thread and might not be immediately available. Navigate() doesn't wait for the document loading to complete.  To be sure the document is complete you can check the ReadyState property for load completion in loop like this:

 

oIE.Navigate("http://www.west-wind.com/")

 

DECLARE INTEGER Sleep IN WIN32API INTEGER nTimeout

DO WHILE oIE.ReadyState # 4

  Sleep(100)

ENDDO

 

Since this is a common task you might want to build a more generic function that returns .T. or .F. and also lets you specify a load timeout to avoid hung applications if the page load fails for some reason. We'll reuse the WaitForReadyStateFunction shown in Listing 7.5 again later.

 

Listing 7.5 – A common task: WaitForReadyState waits until the document has finished loading.

FUNCTION WaitForReadyState

LPARAMETERS lnReadyState, lnMilliSeconds

IF EMPTY(lnReadyState)

  lnReadyState = 4

ENDIF

IF EMPTY(lnMilliSeconds)

   lnMilliSeconds = 4000

ENDIF

 

DECLARE INTEGER Sleep IN WIN32API INTEGER nMSecs

lnX = 0

DO WHILE this.oBrowser.ReadyState # lnReadyState AND ;

         lnX < lnMilliSeconds

  DOEVENTS

  lnX = lnX + 1

  Sleep(1)

ENDDO

 

IF lnX < lnMilliSeconds

  RETURN .T. && Not timed out

ENDIF

 

RETURN .F.

 

With the document loaded you can access just about every detail of the HTML page with a very rich object model that lets you access every HTML tag and control in the document. From here you can retrieve and change content in the HTML document, even set values in a form and submit the form automatically. I'll talk more about accessing the HTML document later, but for now just understand that you can read and manipulate all parts of the HTML document directly through the object model through both the IE Application object and the Web Browser control.

Special Operations

The Web Browser can be accessed and controlled pretty cleanly through several command interfaces. For example the ExecWB method allows command access to most operations that the User Interface provides. For example the following code allows you to print the currently loaded document:

 

oIE.ExecWb(6,0,0,0)

 

ExecWB has a number of parameters that can be passed the second of which usually controls whether IE just does it or goes through its usual set of prompts. The above prompts to print without a prompt you can use:

 

oIE.ExecWb(6,2,0,0)

 

The parameter of 2 - LECMDEXECOPT_DONTPROMPTUSER   - causes the UI to be suppressed.

 

There are OleCommandIds (which you can look up on MSDN by looking up ExecWB) that deal with Printing, Previewing, Saving, Cut and Paste operation – most of the things that you see on the IE menu.

 

Along the same lines is the HTML Document’s  execCommand() method which allows execution of document specific commands. This command has fewer control UI options and in this case belongs to the MSHTML browser model, so it will only work on HTML documents and is subject to the browser's security restrictions. For example, the following allows you to save the currently loaded HTML document to disk (with prompt).

 

oIE.Document.execCommand("saveas")

 

Both of these commands are highly useful if you need to interactively drive IE’s user interface.

Capturing Browser Events

Using the Application object you can also capture the various events that the IE object exposes. Events allow you to trap things like statusbar messages, completion events, link submission events and many other operations. To capture and handle events you need to implement the following interface:

 

DEFINE CLASS WebBrowserEvents as Custom

   IMPLEMENTS DWebBrowserEvents2 IN  "SHDOCVW.DLL"

… implementation of event methods here

ENDDEFINE

 

The implementation of this class consists of about 30 handler methods that must be provided. The easiest way to do this is by using the Object Browser which will automatically generate the complete interface for you in source code. To do this:

 

  1. Open a new PRG file and leave it open
  2. Open the Object Browser
  3. Go to the COM Libraries tab and select Browse
  4. Select <your system dir>\shdocvw.dll
  5. Open the class and select the Interfaces tree
  6. Select the DWebBrowserEvents2 interface
  7. Drag and Drop the Interface into the new empty PRG file (Figure 8)
  8. Change the classname of the dropped interface to WebBrowserEvents
  9. Change the path to the SHDOCVW.DLL file by stripping off the path leaving just the filename

 

 

Figure 8 -  Dragging and dropping the DBWebBrowserEvents2 interface to a PRG file generates an implementation for the interface with all empty methods that you can override.

 

Finally let's create the calling code above the generated interface that you can use to hook up the events and test operation. The code in Listing 8 shows the calling code, and the event interface class with a couple of implemented handler methods.

 

Listing 8 – Implementing the IE Web Browser events with EVENTHANDLER()

LPARAMETERS lcURL

 

IF EMPTY(lcUrl)

   lcURl = "http://localhost/"

ENDIF

 

 

PUBLIC oIe as InternetExplorer.Application

IF TYPE("oIe.Height") # "N"

   oIE = CREATEOBJECT("InternetExplorer.Application")

  

   oIEEvents = CREATEOBJECT("WebBrowserEvents")

   EVENTHANDLER(oIE,oIEEvents)

   TRY

      oIE.Navigate("about:blank")

   CATCH

      *** Ignore Default interface error

   ENDTRY

  

ENDIF  

 

oIE.Height = 600

oIE.Width = 800

oIE.ToolBar = .F.

oIE.AddressBar= .F.

 

oIE.Visible = .T.

oIE.Navigate(lcURl)

 

RETURN oIe

 

DEFINE CLASS WebBrowserEvents AS Custom

  IMPLEMENTS DWebBrowserEvents2 IN  "SHDOCVW.DLL"

 

   PROCEDURE DWebBrowserEvents2_StatusTextChange(;

                           Text AS STRING) AS VOID

  

   WAIT WINDOW NOWAIT Text

   ENDPROC

 

   PROCEDURE DWebBrowserEvents2_BeforeNavigate2(;

        pDisp AS VARIANT, URL AS VARIANT, ;

        Flags AS VARIANT, TargetFrameName AS VARIANT,;

        PostData AS VARIANT, Headers AS VARIANT,

        Cancel AS LOGICAL @) AS VOID

  

   IF ATC("www.west-wind.com",Url) = 0 AND ATC("about:blank",URL) = 0

      MESSAGEBOX("Not allowed to go to " + URL)

      Cancel = .T.   && Don't allow the navigation

   ENDIF

  

   ENDPROC 

  

   … other inteface methods required but excluded here

ENDDEFINE

 

There are a couple of important things to note about this code. First note how the event interface is hooked up:

 

oIE = CREATEOBJECT("InternetExplorer.Application")

oIEEvents = CREATEOBJECT("WebBrowserEvents")

EVENTHANDLER(oIE,oIEEvents)

 

You first create the Application object, then the event object and use VFP's EVENTHANDLER() function to tell oIEEvents to handle the events fired by oIE. Next notice the TRY/CATCH error handler used to navigate to about:blank which is IE's default blank page. There's a bug generated in the FileDownload() event handler due to a unusual parameter being passed to VFP that it can't handle. The first navigation always fails and the TRY/CATCH code handles this by navigating to the default page and simply ignoring the error, and only then navigating to the real page.

 

The event implementation here does two things: It captures the StatusTextChange event and simply echos the text into a Wait Window. You'll see the wait window change frequently as a page loads.

 

The second event handled is probably the most useful event surfaced by the IE object model. BeforeNavigate2 fires before IE navigates to a new URL. This method is passed a URL and all information about the page submitted (although the other parameters are always empty – so don't count on capturing the POST data for example). There's also a Cancel parameter which is passed by reference and can be set to .T. to disallow the URL to be navigated. The sample code (Listing 7.5)  disallows all access to non-West Wind links and displays a message box if another URL is accessed which might be useful if you want to control where users can browse to from your application.

Object Lifetime

Lifetime management for the IE object is somewhat tricky because the object lives outside of your application and you can close Internet Explorer externally.  Note that I have this code:

 

PUBLIC oIE as InternetExplorer.Application

IF TYPE("oIe.Height") # "N"

   oIE = CREATEOBJECT("InternetExplorer.Application")

 

to determine whether IE exists. The reference is loaded as PUBLIC object so if you run this multiple times with different URLs the same IE object instance is reused. However, if you close Internet Explorer interactively (Alt-F4 or clicking the X) your Fox code still leaves oIE set as an object reference, but then this object now points at an invalid instance of IE. By checking a property to on the IE object you can see if the object reference is still good and if it isn't you can recreate it. Another option is to use the OnQuit event and in it release the reference, but this works only if you use a PUBLIC var as I've done here.

 

In a real application you also have to figure out how to hang on to the instance you have created since you likely want to use it and keep it around while the main application runs. Here I've used a PUBLIC variable, which, even though I hate using them, is reasonable in this case of  a truly independent object from the application. Alternately you can attach the instance to an Application object, or if it's specific to a Form to the Form instance.

 

Just make sure that you always account for closing the object properly by first setting Visible to .F. then releasing the instance. One way to do this is to create a wrapper object that manages the lifetime for you properly by firing the Destroy event on the wrapper which properly releases the underlying IE object reference.

The Web Browser Control

The Microsoft Web Browser ActiveX control also provides all of the functionality I've described thus far for the Internet Explorer Application object. The major difference is that the control lets you embed the Internet Explorer directly into your own Visual FoxPro forms as opposed to an external desktop window.  Also the events that IE publishes are exposed as native events of the ActiveX control so you don't need to implement an interface to handle events. Which one of these components you use is really up to your application scenario, but in general I find that I have more control over the lifetime of the control if I use the ActiveX control and more options in how I can mix VFP and Web browser content.

 

To use the Web Browser control on a Fox form, use the following steps:

 

  1. Open a new FoxPro Form
  2. Use the ActiveX control picker and drop Microsoft Web Browser Control onto the form
  3. Name the control oBrowser
  4. Set the Refresh Event code to NODEFAULT

 

The last step is necessary as the Refresh event causes an error in VFP to occur. Figure 9 shows the sample form containing the Web Browser control.

 

Figure 9 – The Web Browser control on a FoxPro form in design mode. Once on the form the control can be fully automated via code, and events handled by simple event methods. This example demonstrates several features of the control.

 

When first run without a URL the control shows an empty surface and an empty HTML document and THISFORM.oBrowser.Document = null. You can use the Navigate() method to then navigate to a specific URL:

 

THISFORM.oBrowser.Navigate("http://www.west-wind.com")

 

If you end up modifying the HTML document in your code or if you want to display an emty document you'll always want to make sure load at least an empty document with:

 

THISFORM.oBrowser.Navigate("about:blank")

 

about:blank is an HTML document that is valid, but contains only <html><body></body></html> and you can then continue to modify the document. For example:

 

THISFORM.oBrowser.Document.Body.InnerHtml = "<h1>Hello World</h1>"

 

Alternately, your Init() of the form can already navigate to a specific URL or file. Remember as with the IE Application object you need to make sure that the document is loaded completely before you start modifying any part of it via code. Use WaitForReadyState() to do this as necessary.

 

In this case the form can be passed a URL parameter which looks like this in the form's Init():

 

* WebBrowser.Init()

LPARAMETERS lcUrl

 

IF !EMPTY(lcUrl)

   THISFORM.Navigate(lcUrl)

ELSE

   THISFORM.Navigate("http://www.west-wind.com/")

ENDIF

 

THISFORM.Resize()

 

The Resize of the form resizes the Web Browser control to fit the form and the end result of the running form without a parameter is shown in Listing 10.

 

Figure 10 – The Web Browser control once navigated displays a full IE client frame inside of the FoxPro form. You can now use events like BeforeNavigate2 to trap navigation to other links.

 

As you can see from the code above the form includes a Navigate() method which in turn calls the browser's Navigate method. Although not essential in this application, in most application contexts this is a good idea to properly wrap the calls. For example, trimming the URL properly of spaces, or performing any fixups of the URL before passing it to the navigate method. In this form the Navigate method deals with checking what the current URL is and only navigating to it if the URL has changed.

 

Listing 9 – The Navigate method of the form keeps track of where we are

* Navigate Method of the form

LPARAMETERS lcUrl

 

IF EMPTY(lcUrl)

   lcUrl = TRIM(THISFORM.txtUrl.DisplayValue)

ENDIF

 

*** Don't navigate if we're on the same URL

IF lcUrl = THISFORM.cCurrentUrl

   RETURN

ENDIF

 

IF !EMPTY(lcUrl)

   THISFORM.oBrowser.Navigate(lcUrl)

ELSE

   THISFORM.oBrowser.Navigate("http://www.west-wind.com/")  

ENDIF  

 

IF EMPTY(lcUrl)

   thisform.cCurrentUrl = "asasdasd"

ELSE

   THISFORM.cCurrentUrl = lcUrl

ENDIF

 

THISFORM.Resize()

 

The form also captures a URL history in the combo box by using the NavigateComplete2 event which fires when the page is done loading. When the form is shutdown the list is then written into a DBF file which is reloaded if it exists so you have a history that persists.

 

Listing 10 – Adding the last URL to the combobox

*** NavigateComplete2 Event ***

LPARAMETERS pdisp, url

 

IF EMPTY(Url)

   RETURN

ENDIF

 

thisform.txtUrl.AddItem(Url,1)

THISFORM.txtUrl.Value = Url

 

*** Drop off the last item if list is too large

IF thisform.txtUrl.ListCount > 25

   thisform.txtUrl.RemoveItem(26)

ENDIF

 

The Web Browser control also includes native methods for things like GoBack(), GoForward(), GoSearch (), GoHome() that you are familiar with in the browser's user interface.

It's not just for Web Content

The Web browser control has many uses that go way beyond the Web. HTML is not a good tool for everything, but it's nice for certain applications that require a rich graphical experience. Sometimes it's just easier to display information as HTML—sometimes it's a good idea to build output dynamically with VFP code into HTML format and use the browser control as the preview mechanism to see what it will actually look like. Figure 11 shows a VFP Desktop application for building HTML Help Files which uses local HTML content dynamically generated to disk to display a preview of the final HTML topic layout in the Web Browser control.

 

Figure 11 – Displaying HTML content doesn't have to come from the Web. In this application the HTML is generated dynamically to disk and previewed from a file for each topic displayed. The rich display of HTML provides interface options that are difficult to achieve with VFP forms.

 

The concept behind the code for this kind of interface is pretty straight forward. The form contains a Web Browser control, which is refreshed based on an action in the VFP user interface. In this case if a user clicks on one of the topics, the data that is stored in the particular Help Interface record is rendered into HTML into a static file called _wwHelp_Preview.htm. The Web Browser control is then Navigated to this page to display the content as HTML in the control.

 

The high level code, triggered by the treeview's NodeClick event is pretty simple and contained in a method called GoTopic():

 

lcHTML = oHelp.RenderTopic() && Create the HTML output

STRTOFILE(lcHTML,this.oHelp.cProjectPath + "_preview.htm")

THISFORM.oBrowser.Navigate("file://" + ;

                  this.oHelp.ProjectPath + "_preview.htm")

 

The Help Builder application is an extreme example for HTML content because HTML Help is, well, all about HTML. But this sort of interface comes in handy in many situations where you want to display data dynamically. For example, I frequently use HTML lists in applications to display lineitem lists where the lineitems have varying numbers of fields to display. Take a look at Figure 12 which shows a list of lineitems with an optional discount. This would be difficult to display using standard VFP controls.

 

Figure 12 – Using HTML inside of standard VFP forms can also provide a clean, modern look as well as help with varying data such as the discount above. HTML also works well for lists displaying memo data which is nearly impossible with standard VFP controls.

 

Any kind of output that needs to stream and has varying sizes per row – memo fields for example – are also good candidates for HTML display. Nested relations (such as many-to-many) are also easier to handle with a streamable output source like HTML, rather than a fixed-field-based format like input fields and grids.

Capturing Navigation Events

This brings up another point that is fairly important if you plan to integrate the Web Browser control. How do you capture clicks on a form and let VFP handle these events? The key to this task is the BeforeNavigate2 event of the Web Browser control. This event fires before the browser navigates to the requested page when you click on any link and lets you trap the URL that you are accessing. It gives you a hook to check the URL and if necessary perform actions in your VFP code to act in response of the URL.

 

For stand-alone applications it's even more important to capture navigation events. Rather than going out to the Internet, applications typically must perform operations before displaying data. For example, in the invoice form above I want to capture an item link the user clicks on, and then bring up the appropriate lineitem form.

 

The trick to this feat is to use a special syntax for 'application' URLs that are structured as directives. In order for a URL to be valid it must have a protocol, followed by a path. Links in the Lineitem list above look like this:

 

VFPS://EDITITEM/10121

 

All Web browser links in the application start with a VFPS:// (VFP Script) protocols, and have an EventId (EDITITEM) and optional parameters that are passed. There's nothing special about the VFPS:// protocol prefix – it's a name I made up, but can be anything as long as it has the :// at the end. The EventID and parameters are then parsed in the BeforeNavigate2 method and call the appropriate forms or functions. Here's the code for BeforeNavigate2() that fires the EditItem() method on the form:

 

Listing 11 – Handling application Urls prefixed with the VFPS:// protocol

* BeforeNavigate2 event handler

LPARAMETERS pdisp, lcUrl, flags, targetframename, ;

                      postdata, headers, cancel

LOCAL lnPK, lcCommand

 

DO CASE

CASE LEFT(UPPER(lcUrl),7) = "VFPS://"

   lcCommand = UPPER(STREXTRACT(lcUrl,"//","/"))

   DO CASE

   CASE lcCommand = "EDITITEM"

      lnPK = VAL(SUBSTR(lcUrl,RAT("/",lcUrl)+1))

      THISFORM.EditItem(lnPK)

      CANCEL = .T.  && don't want to actually show lcUrl

   CASE lcCommand = "DELETEITEM"

      lnPK = VAL(SUBSTR(lcUrl,RAT("/",lcUrl)+1))

      THISFORM.RemoveLineItem(lnPK)

      CANCEL = .T.  && don't want to actually show lcUrl

   ENDCASE

ENDCASE

 

The key here is to keep these 'application' URLs simple so you don't have to do extensive parsing. Here only a PK is passed as a parameter. In each case the navigation is cancelled by setting the CANCEL flag to .T. This is important as you don't want to update the Web Browser control with new data. Instead you want to pop up a new FoxPro form to display data – the initial display of the Web Browser control is only changed when the invoice is refreshed (ie. An item was added or deleted or a new invoice was selected altogher).

 

If you plan on using the Web Browser control frequently with link handlers you might want to subclass the control as shown in Listing 11.5.

 

Listing 11.5 – Implementing a Web Browser Control subclass that handles VFP Script

FUNCTION BeforeNavigate2(pdisp, url, flags, ;

    targetframename, postdata, headers, cancel)

LOCAL lcParms

LOCAL ARRAY laParms[1]

 

DO CASE

  CASE LEFT(UPPER(url),7) = "VFPS://"

       lcParms = SUBSTR(Url,8)

       ALINES(laParms,lcParms,"/")

       THIS.OnVFPScript(@url,@cancel,@laParms)

ENDCASE

FUNCTION OnVfpScript(Url, Cancel, Parms)

ENDFUNC


FUNCTION Refresh
NODEFAULT
ENDFUNC

 

By doing so you can now easily create VFPS:// links and simply look at the Parms object to retrieve the keyword and any other additional parameters. The handler code in OnVfpScript() for an invoice click now reduces to:

 

CASE UPPER(Parms[1]) = "EDITITEM"

    cancel = .T.

    THISFORM.EditItem( val(Parms[2]) )

The HTML Object Model

The ability to manipulate the document from the container application is extremely powerful. To run the following examples from the command window, you can do the following:

 

DO FORM WebBrowser NAME oForm LINKED

 

*** for space store the browser in o
o = oForm.oBrowser  && Use o for brevity

 

Note that what follows will work with any reference to a Web Browser control (o for brevity here) or an InternetExplorer.Application object, assuming a document is loaded. For example, once you have a document loaded, you can access and manipulate the HTML content with code like this:

 

*** Return HTML minus the header

? o.Document.Body.InnerHTML     

 

*** Return text without HTML tags

? o.Document.Body.InnerText       

 

Both commands access the HTML Document object of the browser, which allows reading and writing HTML directly from and to the browser window. The Body object encapsulates all of the HTML between the <body> and </body> tags in the document, and the InnerHTML property returns all of that HTML text. InnerText retrieves the same text with all HTML formatting removed. You can also get the OuterHTML:

 

*** Return HTML minus the header

? o.Document.Body.OuterHTML

 

which includes the actual <body></body> tags, while InnerHTML does not. InnerHTML and OuterHTML are supported by most objects in the Document object. You can also retrieve the entire document (in IE 5.0 and later) with:

 

? o.Document.DocumentElement.OuterHtml

 

In addition to reading, you can also write to the document, which allows you to dynamically change the HTML of a document:

 

o.Document.Body.InnerHTML = "<h1>Hello World from Visual FoxPro</h1>"

 

The HTML on the right replaces the previously loaded document body, effectively giving the appearance of a new document. Note that you can't replace the entire HTML document, but only the body of the document. You can modify individual header tags using the various header collections, but there's no way to set the full HTML document short of reloading from a new document. You can't assign text to the DocumentElement object.

 

This explains why I used file output in my HTML Help project above, because the user could customize the output template, including the HTML headers, which include complex style sheets that might be stored in the header of the HTML template. While the document exposes the headers as separate objects that can be modified, it gets a lot more complicated than simply replacing the entire document text. Because the templates have extensive style sheets, meta tags and other header elements, I opted to go the simpler route of simply writing to file and reloading the document. You might think writing to file is slow, but the difference between writing the file and changing the document is not noticeable for anything but the smallest documents as most of the overhead is actually in rendering the HTML not loading it.

Tag collections

Your access is not limited to the entire document as a whole, but you can directly manipulate any portion thereof. All HTML tags in a document are accessible via collections that allow iterating through all instances of each tag. For example, if you wanted to get all the bold text in a document, you could do this:

 

FOR EACH loTag IN o.Document.All

  IF UPPER(loTag.Tagname) == "B"

     ? loTag.InnerHTML

  ENDIF

ENDFOR

 

Document.All is the master collection that allows you to go through every single tag in the document—which is exactly what the code above does. It runs through every tag and checks to see whether it's a bold tag, and if so, retrieves it for display.

 

Alternately you can also use the GetElementsByTagName() function to retrieve specific types of tags which is a little more efficient. For example, it might be useful to parse all HREF links in a page which can do simply with:

 

loNodes = o.Document.getElementsByTagName("A")

FOR EACH loNode IN loNodes

  ? loNode.href

ENDFOR

 

 

If you've ever wanted to build a Web Spider or a link validator this should make your job rather easy! There are also a number of other predefined collections, such as Forms, Anchors, Images, Frames and Scripts.

Custom ID tags and modifying Form content

Internet Explorer also supports the concept of custom ID tags, which are used by the HTML object model to parse the HTML text. For example, take a look at the following HTML code:

 

Visual FoxPro Version: <b ID="VFPVersion">n/a</b>

 

<form method="POST" action="CustomBrowserTags.htm">

  <p>Name: <input type="text" name="txtName"

                  ID="txtName" size="20"></p>

  <p>Company: <input type="text" name="txtCompany"

                     ID="txtCompany" size="20"></p>

  <p><input type="submit" value="Save" name="btnSubmit">

</form>

 

Notice the ID tags VFPVersion, txtName and txtCompany in the document. With this document loaded in the browser, you can easily get at these tags. Notice that the original document has n/a as the version number. To insert the real VFP version number from your code, do this:

 

o.Document.all.VFPVersion.innertext = Version()

 

When you do, n/a changes to the VFP version number in the HTML document! Think about this for a second—this mechanism allows you to build very easy data binding from a VFP host container into an HTML document, if necessary, simply by delimiting every piece of dynamic data in the HTML page with its own custom ID tag. If you use a consistent mapping naming scheme between properties and using Assign methods on your objects you can have an HTML document update whenever a property is changed for example.

 

Using a custom ID tag is an easy way to "bookmark" any area of the HTML document. All you need is Document.all.IDTag, and you get back an object reference to that tag. Once you have a reference, you can read and write the content and call methods on the object. The type of the object and the functionality available to it will vary based on the type of HTML object being referenced. The above code demonstrates how you can easily update an HTML form in a form's Refresh event with values from the currently selected record in a table. This is more impressive with the HTML form above. From VFP you can now do this:

 

*** Retrive the HTML form variable

lcName = o.Document.all.txtName.value

lcCompany = o.Document.all.txtCompany.value

 

*** Set the HTML form variable

o.Document.all.txtName.value = "Rick Strahl"

o.Document.all.txtCompany.value = "West Wind Technologies"

 

This allows you to read and write values inside the HTML form! Using this mechanism, you can essentially bind Fox data directly to an HTML document displayed through your application. To find out what type of object you're dealing with, you can always use the following:

 

? o.Document.All.txtName.ID

? o.Document.All.txtName.classname

? o.Document.All.txtName.tagname

 

although the classname often is not set. You should also get into the habit of checking for types before actually accessing these objects. If something is mistyped in your document or can't be found, or if you have a duplicate tag entry, you can get unexpected results. Trying to access an invalid object will result in a COM exception, so the up-front check will save you from having to catch those errors later. Use the TYPE() function to check for type “O”.

DIV and SPAN  tags for blocks of HTML

You can also mark entire sections of HTML with an ID. Above I used the ID attribute on a <B> bold tag. ID tags can be applied to any HTML element on a page. This includes range tags like DIV and SPAN, which are used to delimit blocks of HTML.

 

<DIV ID="Detail" Style="display:none">

Some HTML text goes here…

</DIV>

 

This HTML snippet won’t be visible when first loaded, but you can access that range with to display it:

 

o.Document.All.Detail.display = ""

 

To hide the text again:

 

o.Document.All.Detail.display = "none"

 

You can also use this mechanism to build HTML into a string via your own code and then assign the code directly to the Div tag area.

 

lcHTML = loHelp.RenderTopic()

o.Document.All.Detail.innerHTML = lcHTML

 

You’ll probably use DIV tags extensively in dynamic documents that show and hide data frequently and documents that you want to embed generated HTML into. DIV tags make great placeholders for blocks of HTML code.


DIV is a block based tag, while SPAN is a paragraph based tag. If you want to delimit some text within a paragraph use the SPAN tag. SPAN will not cause an HTML line break to occur, while DIV does. Otherwise the concept of SPAN tags is identical to DIV.

Selections and text ranges

It's also possible to access HTML elements as selected text inside the HTML document. For example, the user could be looking at a large text document and highlight an article that needs to be imported into a specific field of the database. You could do:

 

REPLACE kb.article WITH o.Document.Selection.CreateRange.htmlText

 

The Selection object (IHTMLSelectionObject) is not the most intuitive—you explicitly have to create a range (IHTMLtxtRange) of selected text. If you pass no parameters to CreateRange, the default selected text is used from the HTML document. Once you have a reference to the selected text, the htmlText property retrieves the actual text.

 

To replace text in a selection, use this:

 

o.Document.Selection.createrange.PasteHTML("My HTML Text here")

 

With this functionality you can almost build your own HTML text editor. But there's an easier way and I'll come back to editing HTML a little later.

Accessing HTML script from your VFP code

Not only can you manipulate the document, but you can also cause things to happen in the HTML page. Once you get an object reference to an object, you can call any of its methods and cause it to do something. For example, you can get a reference to a button and call its Click method:

 

o.Document.all.btnClear.Click()

 

You can also call any script code directly using the document's Script object. Assume you have a method called ValidateInput() inside the HTML document as VBScript or JavaScript. You can cause that to fire with the following code:

 

o.Document.Script.ValidateInput("parameter")

 

This essentially allows you to control an HTML application through code fired from a VFP host application. Note that any script code must be in the HTML document's header to be accessible through Document.Script. If you have multiple blocks of code in script tags, only the first block is recognized, so keep it organized in one place when creating pages accessed externally.

Passing an object

Since you can call functions in the HTML page you can also pass information from VFP to the HTML page including VFP objects. But it's a little more tricky as you have to make sure that you create a public reference to the object if you want the script page to be able to use this object independently. The HTML code in Listing 12 demonstrates a function called SetVFPObject that accepts a VFP form object and makes it available to the script page.

 

Listing 12 – Passing a VFP object to a script page requires a public object reference

<body>

<script LANGUAGE="VBSCRIPT">

DIM oVFP  'public variable!

 

Function SetVFPObject(loVFP)

   Set oVFP = loVFP 'assign to public

End Function

Function MoveParent()

   oVFP.Top = 0

   oVFP.Left = 0

End Function

</script>

<form method="POST" action="CustomBrowserTags.htm">

  <input id="btnMove" type="button" value="Move Parent Form to 0"

         onclick="MoveParent()"></p>

</form>

</body>

 

The global variable oVFP will receive the object reference that is set by the function call to SetVFPObject. Inside Visual FoxPro you can pass the reference down like this after the document has loaded:

 

o.Document.Script.SetVFPObject(THISFORM)

 

The above HTML form has a button that calls the MoveParent script function which, in turn, uses the object reference set by the previous call. When you click it inside the browser, the form that hosts the browser window will move to 0,0 on the VFP desktop. If you want to be really twisted you can also use VFP to call this script:

 

o.Document.Script.SetVFPObject(THISFORM)

WaitForReadyState(4,5000) && wait for doc to load
o.Document.btnMove.Click()

 

This is an obvious mechanism that you can use to capture nonstandard and control-level events inside the browser and pass them back to your Visual FoxPro application. You can use any object, not just a form. You could, for example, pass in a complex business object that could validate input directly inside the HTML script.

 

While this is very cool, it's important not to lose sight of what you want to accomplish. It might simply be easier at times to generate HTML pages with the data already embedded, rather than to manipulate an existing page through extensive script code with VFP object references. Simply put, using the browser to store logic can make applications more difficult because the logic is split up in multiple places. But there are also good uses for it, especially when the interface requires a Web front end that must run with DHTML, or when the display features of HTML provide clear advantages over standard form-based output.

Document Object Sample Application

To demonstrate many of the HTML Document Object features discussed here, the source code for this article includes a form called IEDemo.scx (shown in Figure 13) that displays invoice information from the TASTrade sample application.

 

Figure 13 – This sample form contains a Web Browser control and demonstrates many aspects of manipulating the HTML document model, handling document events and passing data between the HTML document and the VFP application.

 

This application mixes a Visual FoxPro form interface with an HTML interface that is dynamically generated. The listbox on the left is a simple VFP list box, and the When() clause of the list causes a method called ShowRecord() to fire, which generates the HTML for each customer record dynamically. The HTML includes form fields (Company, Name, Address etc) as well as the list of invoices. Listing 13 shows the code for the ShowRecord() method.

 

Listing 13 – The ShowRecord method renders HTML from a template stored on disk

* Function ShowRecord()

 

*** Generate HTML for the invoice list

pcInvoiceList = THISFORM.ShowInvoices(customer.cust_id)

SELE Customer

 

*** Template page containing <%= expression %> tags

lcPage = THISFORM.cHTMLPagePath + "customer.wcs"

 

*** Read the file and merge the expressions into it

lcContent = FILETOSTR(lcPage)

lcContent = STRTRAN(lcContent,"<%=","<%")

lcHTML = TEXTMERGE(lcContent,.F.,"<%","%>")

 

*** Write the result out to a local file and show in browser

StrToFile(lcHTML,"_preview.htm")

THISFORM.oBrowser.Navigate(CURDIR() + "_Preview.htm")

 

*** Wait for doc to load

IF THISFORM.Waitforreadystate(4,8000)

     *** Then pass VFP form to the script page

     TRY     

        THISFORM.oBrowser.document.script.SetRef(THISFORM)

     CATCH

        WAIT WINDOW NOWAIT "Error loading customer..."

     ENDTRY

ELSE

     WAIT WINDOW "Customer Load Failed..." nowait

ENDIF

 

You'll notice that this code does not generate HTML directly but rather stores the HTML in an external page on disk called customer.wcs in the .\templates directory. This template contains the base HTML which includes ASP style <%= %> expression tags that hold the data that Visual FoxPro is providing. For example, the textboxes are loaded like this:

 

<input name="txtCompany" size="32" value="<%= customer.company %>

 

Where customer.company is the customer cursor from of the TasTrade application. This provides the inbound databinding for the form controls. The actual Customer.wcs template is loaded from disk into a string and then fixes up the expression tags so they can be used with VFP's TEXTMERGE() function which then merges the data into the template. The resulting string is then written to a file and displayed via the browser's Navigate method.

 

The invoice listing is generated as HTML via code in the ShowInvoices() method. This method uses a more brute force approach using the TEXTMERGE command to generate blocks of HTML with embedded expressions in code. Listing 14 shows the code used to generate the body of the invoice list inside of a SCAN loop. Note that here the default << >> delimiters are used for the embedded VFP expressions.

 

Listing 14 – Using TEXTMERGE to generate dynamic HTML output from VFP code

… more code to generate the table header

SCAN

    TEXT TO lcOutput ADDITIVE NOSHOW TEXTMERGE

    <TR>

        <TD Align=Center><a href="vfps://Orderid/<<UrlEncode(Order_id)>>">

                                     <<Order_id >></a></TD>

        <TD Align=Center><<TRANSFORM(Order_date)>></TD>

        <TD Align=right><<TRANSFORM(Order_amt,"999,999.99")>></TD>

        <TD Align=Center><<Transform(Shipped_on)>></TD>

    </TR>

    <TR ID=Order<<Order_id>></TR>

    ENDTEXT

ENDSCAN

… more code to generate the total

Return lcOutput

 

Note that Listing 13 retrieves the result from ShowInvoices() in Listing 14 into a variable pcInvoiceList, which is simply embedded into the Customer.wcs HTML template with:

 

<%= pcInvoiceList %>

 

After Navigate() has been called the code needs to wait for the document to complete loading so we can access the document using WaitForReadyState() described in Listing 7.5. Once loaded the code goes ahead and tries to call a script function in the HTML document, passing the VFP form as a parameter. The purpose of this is to allow the HTML page to call back into the VFP application when a form field is changed and thus update the database. It does so with a very simple function in the HTML document called SetRef which assign the VFP form to a public variable:

 

<SCRIPT language=VBScript>

Public goForm

 

Sub SetRef(ByVal loForm)

  SET goForm = loForm

End Sub 

Sub txtCompany_onchange()

  goForm.UpdateField "customer.company",me.value

End Sub
… additional field OnChange event handlers omitted

</SCRIPT>

 

The OnChange event handler for each of the form controls is then created which fires whenever a change is made to one of the input fields. goForm now points at our VFP form and we can call back into the form in this case calling the UpdateField method which saves the value and pops up a MessageBox as shown in Figure 13.

 

* Function UpdateField

LPARAMETER lcField, lcValue

 

replace &lcField with lcValue

 

MessageBox("VFP data updated:" + CHR(13) +lcField + ;

      " with " + CHR(13) + lcValue,64,"IE Form Demo")

 

Finally the application handles clicks of the individual Invoice links through the BeforeNavigate2 event. The links are marked with VFPS:// protocol prefixes and look like this:

 

vfps://Orderid/+11011

 

which is handled by the following code:

 

*** HANDLE any URL's that have VFPS

CASE LEFT(UPPER(url),7) = "VFPS://"

   lcCommand = UPPER(EXTRACT(url,"//","/"))

   DO CASE

   *** Order Id key - show that order

   CASE lcCommand = "ORDERID"

      lcOrderId = SUBSTR(url,RAT("/",url)+1)

      THISFORM.ShowInvoiceForm(lcOrderid)

      CANCEL = .T.  && don't want to actually show URL

   ENDCASE  

ENDCASE

 

The BeforeNavigate2 handler method parses the URL and and retrieves the OrderId and then calls the ShowInvoiceForm() method with the order Id to display yet another HTML view that displays the specified order.

 

Although this example may not be a best use case for utilizing HTML to display data it does demonstrate how you can interact with HTML once it's been generated and displayed in the Web Browser control. I find that there are lots of applications where displaying content in HTML is preferable to displaying standard form interfaces even if it is a little more work.

Editing HTML

Finally the HTML document object model supports editing of HTML content. This is surprisingly easy using:

 

o.Document.DesignMode = "on"

 

Set the value to "off" and you're back in display mode. DesignMode essentially modifies the document in memory and you're responsible for writing out the document text to disk in some fashion after you're done editing.  To save the document you can do:

 

lcHtml = o.Document.DocumentElement.outerHtml

STRTOFILE(lcHTML,"d:\temp\defaultpage.htm")

 

Another option with the same result but user interaction is:

 

o.Document.execCommand("saveas")

 

which lets you dynamically save the HTML content only to disk. If you'd like to save the document using IE's native Save As capability which also saves images, style sheets and other embedded linked content to a directory with the same name as the page, you can use:

 

o.ExecWb(4,0)

 

The sample form Web Browser.scx demonstrates most of these features and is shown again in Figure 14, displaying a document that is in Edit mode.

 

Figure 14 – Editing HTML is as easy as setting the Document's DesignMode property to "on". Marking up text involves either using built in editing features like resizing of objects or built in hotkeys like Ctrl-B, or by manipulating the HTML document via Text Ranges programmatically.

 

When you are editing a document you can simply position the cursor anywhere and modify the text as you see fit. IE includes a common set of built-in hotkeys like Ctrl-B for bold, Ctrl-I for Italic, Ctrl-K for inserting a hyperlink using common Microsoft application hotkeys. If you need to perform more complex HTML editing you will need to manipulate the HTML document itself via Selection Ranges. For example, the red text in Figure 14 was typed in, then highlighted and then modified via the following click code in the Bold button:

 

loSelection =WebBrowser.oBrowser.Document.Selection

lcHtml = loSelection.CreateRange.HtmlText

lcNew = "<span style='color:Red;font-weight:bold'>" + ;

        lcHTML + "</span>")

 

loRange = loSelection.createRange

loRange.pasteHTML(lcNew)

 

This code captures the current selection of the document into a string and marks it up with a <span> tag that provides the formatting. It then creates a new text range and pastes the new text into it. You can use this mechanism to paste in new HTML or replace existing HTML.

 

It's relatively trivial built a reasonably sophisticated editor that uses these simple tools to embed text into a document. You can even perform some layout tasks like resizing object such as table columns shown in Figure 14 or deleting a selected object by pressing Delete. It's even possible to allow editing in WYSIWYG mode as well as providing Source View to edit HTML manually if you can save the document between switches from source to WYSIWYG mode.

 

The HTML editing works well for editing documents, but I've found that it's difficult for creating HTML documents from scratch unless you provide at least a baseline. If you're used to FrontPage or even the VS.Net editor you'll find that you'll immediately miss the ability to right click and set properties. To build a truly user friendly HTML editor requires some work, but for basic layout and editing tasks of existing documents the Web Browser control as is works actually really well.

Handling MSHTML Document Events

As you probably are aware, the HTML document object fires events. Every one of the objects displayed inside of the browser also have a whole long list of events associated with it. Remember earlier we were implementing the IWebBrowserEvents interface to hook events in the Web Browser Control (or IE) itself. But these events are container events. In this section I’ll talk about handling document events. You may not often have a need to hook event documents but when you do it’s usually a pretty urgent matter.

 

The document and each of the elements inside of the document all fire events and you can intercept these events by implementing the appropriate event interfaces. Keep in mind that the HTML DOM is hierarchical – there’s the document that contains the header and body, and the body in turn contains text, images, links and all sorts of other HTML elements. Events that fire, fire from the inside out, so when you click on an image or hyperlink. Some controls handle certain events – a hyperlink handles a click (unless you override the event yourself). But an image doesn’t handle a click by default, and so the click gets passed up to the next container say a table column, then to the table row, the table and finally the body, and then the document. The Click event registers in all of these objects even though most of these objects pass this event straight through unless a custom handler is implemented. Anywhere along the line you can use the Window’s event object – oDoc.parentWindow.Event – and its CancelBubble property to stop this flow of events.

 

To intercept these events you have to implement an Event interface and then hook it to the object or objects that you want to have handle these events. Get out your trusty VFP Object Browser or better yet Visual Studio’s OleViewer and look for Disp Interface with names like HtmlDocumentEvents, HtmlImgEvents, HtmlElementEvents etc. You have to use the specific handler to handle an event for a particular element type.

 

Ok time for an example. A common thing you might want to do when you’re hosting the Web Browser Control is to suppress the Context menu. Right now in all the samples I’ve shown you can simply right click the document and see the standard IE context menu that includes View Source, Refresh, Save As etc. This behavior may be a problem for your application, for example if you wanted to protect script code that lives inside of the page.

 

The easiest way to do this is to handle the Document’s OnContext event. To do this follow these steps:

 

  • Open the Object Browser in VFP
  • Open \windows\system\mshtml.tlb
  • Go to Interfaces and look for HtmlDocumentEvents and select
  • Open a new or existing Program file and drag the interface into the source code

 

 

An long interface definition should be created that looks like this:

 

DEFINE CLASS HTMLDocumentEvents AS session OLEPUBLIC

   IMPLEMENTS HTMLDocumentEvents IN "MSHTML.TLB"

 

   PROCEDURE HTMLDocumentEvents_onclick() AS LOGICAL

   * add user code here

   ENDPROC

 

   PROCEDURE HTMLDocumentEvents_ondblclick() AS LOGICAL

   * add user code here

   ENDPROC

 

   PROCEDURE HTMLDocumentEvents_oncontextmenu() AS LOGICAL

   * add user code here

   ENDPROC

 

   … many more methods

ENDDEFINE

 

Make sure you remove the path from the MSHTML.TLB definition and rename the class from the default MyClass to HtmlDocumentEvents as I’ve done above. Next I suggest that you leave this interface implementation as your base and create a subclass of it and implement your code for the OnContextMenu handler there:

 

DEFINE CLASS wwHtmlDocumentEvents as HtmlDocumentEvents

 

   PROCEDURE HTMLDocumentEvents_oncontextmenu() AS LOGICAL

   * add user code here

   MESSAGEBOX("No Context Menu for you buddy!")

   RETURN .F. && Surpress the context menu

   ENDPROC

 

ENDDEFINE

 

This is the handler code from which you can fire your own code to do whatever you need. Notice that in this case the handler has a return value that determines whether the original functionality fires (.t.) or not (.f.). The final step is to hook up the event to the document object.

 

oIE = CREATEOBJECT("InternetExplorer.Application")

oIE.Visible = .T.

 

oIE.Navigate(lcUrl)

 

IF !WaitForReadyState(oIE,4,10000)

   RETURN .Null.

ENDIF

 

oDoc = oIE.Document

 

*** Hook up the events

oEv = CREATEOBJECT("wwHTMLDocumentEvents")

EVENTHANDLER(oDoc,oEv)

 

Make sure you don’t hook up events until the document is loaded so use the WaitForReadyState() function I introduced earlier. Create the event object and use EVENTHANDLER() to bind the event to the COM object. Now when you run access the page and right click you will see the MessageBox pop up and not IE’s context menu.

Event Information

Let’s do one more of this to demonstrate retrieving information about the event. In the above case we simply respond to the event. If you need more information about where the event was fired from or information like screen information or mouseclick info you can get this info from the generic Event object that exists on the Window object.

 

Let’s hook another event – a click on an image. To do this proceed as above but this time create the interface implementation for HtmlImageEvents. Create a subclass like this:

 

DEFINE CLASS wwHtmlImgEvents as HtmlImgEvents

   oDoc = .null.

  

   PROCEDURE HTMLImgEvents_onclick() AS LOGICAL

   * add user code here

  

   MESSAGEBOX(   this.oDoc.parentWindow.event.srcElement.outerHTML )

     

   RETURN .F.

   ENDPROC

ENDDEFINE

 

In this handler we’re going to echo back the HTML for the image when you click on it. Notice that I have a reference to the Document object here which needs to be set when the object is created. This is so that the event can find a reference back to the Window and then to the Event object.

 

To hook this up the code looks like this after the document has been loaded:

 

oDoc = oIE.Document

  

oLogoEv = CREATEOBJECT("wwHTMLImgEvents")

oLogoEv.oDoc = oDoc

 

*** First Image in Document

LOCAL loImage as MSHTML.HTMLImg

loImage = oDoc.images[1]

 

EVENTHANDLER(loImage,oLogoEv)

 

Run the code again and click on the first image in the document and you should see a dialog that shows the HTML for that image. What if you want to hook this functionality to all images in the document? You’d have to loop through the image collection and hook up this event object:

 

FOR EACH Img in oDoc.Images

   EVENTHANDLER(Img,oLogoEv)

ENDFOR

 

Easy enough. But keep in mind that whenever the page reloads you have to reattach some of these event handlers. Yes, some but not all! The Document object does not reload. But each of the individual elements do. This means you have to be careful with the document only setting it once.

 

They go away as soon as the page is reloaded because you’re basically getting a whole new document object. If you’re using the Web Browser control this means you have to hook up in NavigateComplete in each and every navigation. For the Document you probably need a lFirstDocLoad property that hooks up the events only once, while all other objects must reload on each navigation.

 

This gets even more complicated if you navigate to a non-HTML page. At that point the MSHTML Document gets destroyed and so you loose the event hookups. Unfortunately I have found no way to easily check and see whether the document loaded is fresh or a reloaded document.

 

A few words of caution

The Web Browser control basically hosts an instance of Internet Explorer in your application. Although most of the functionality I've shown here works in all versions of IE since 4.0 some functionality, like editing was introduced only in Version 5.5. So if you provide this functionality be sure you know what versions of IE are required so you can pass those requirements on to end users. To be safe require at least IE 5.5.

 

Also remember that your application is now essentially hosting Internet Explorer and the resulting memory usage for your application will rise accordingly. Use of Internet Explorer generally adds at least 10 megs of memory usage to your application as it appears in Task Manager. Thus if you have only a couple of small places where you want to utilize the control it may not be worth to have this overhead. However, the IE object is very forgiving with memory usage if no longer used and readily returns memory to VFP and/or Windows and if you were using IE externally (via COM or ShellExecute) you would still incur the same overhead on the system – it just wouldn't show up in your app. I find that I have enough uses for the control in most of my apps that I can usually justify the memory usage without a second thought.

Internet Explorer Controls Control Summary

The Internet Explorer Application object and the Web Browser control provide a rich environment for utilizing rich content in your own applications whether it's from Web content or with files from the local disk. It provides the ability to display a variety of content in a common browser environment as well as allowing you some control over how the browser behaves via events. If you're dealing with HTML you get a full tool chest full of functionality to manipulate and automate just about every aspect of the HTML document including the ability to edit the HTML. The uses for this technology are limited only by your imagination…

 

All of the tools I've shown here make it possible to extend your application with very little effort. HTML content especially can provide so much flexibility to applications and can easily become an invaluable tool to most applications once realized. Most of the features I've shown are really easy to implement and work without any third party tools, so everything you need is really for once right at your fingertips.

 

If you haven't played with this stuff before, by all means try it out. Believe me you'll find uses for it quickly and it'll be hard to stop the idea once you get going...

 

As always you can reach me via email at rstrahl@west-wind.com or even better on our Message Board at http://www.west-wind.com/wwThreads/Default.asp?Forum=Code+Magazine.

 

Source Code for this article:

http://www.west-wind.com/presentations/shellapi/shellapi.zip

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

By Rick Strahl

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows Server Products, .NET, Visual Studio and Visual FoxPro. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/ or contact Rick at rstrahl@west-wind.com.

 

 

 

 

 

 

 

 

 

 

 

 

 

Click Here to Pay Learn More Amazon Honor System
  White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |