52
Webcrawl a blog to retrieve all entries locally: RSS on steroids Today’s sample shows how to create a web crawler in the background. This crawler starts with a web page, looks for all links on that page, and follows all those links. The links are filtered to my blog, but generalizing the code to search the entire web or some other site is trivial (if you have enough disk space<g>). (VB.Net version to appear soon on this blog.) I was doing a search on my blog for “ancestors” via the Search box on the sidebar on the left, and there were no results. Strange, I thought, so I used MSN search for my site: http://search.msn.com/results.aspx?FORM=TOOLBR&q=ancestors+site%3Ahttp%3A %2F%2Fblogs.msdn.com%2FCalvin_Hsia%2F That search succeeded: it came up with the expected blog entry . This incident reminded me of the fact that I’ve done a lot of work to create my blog, but I depend on a 3 rd party to maintain it. There are hundreds of code samples, with links to references. If the blog server were to disappear for some reason, so would all my content. I wanted to retrieve all my blog content into a local table. Then I can manipulate it any way I want. In particular, suppose I want to read my entire blog. I would have to do a lot of manual clicking to get to the month/day of the post, and then I might have missed something because I’m manually crawling. That’s pretty cumbersome. Also, I can have all of a blog available while offline, updating when connected. So I wrote a code sample below that crawls my blog, looking for all the blog posts, and shows them in a form which has search capability. Because it’s all local, searching and navigating from post to post is extremely fast. The entry is displayed in a web control, so the page looks just like it would online and the hyperlinks are all live. You can start a web crawl by pushing the Crawl button. You can interrupt the web crawl by typing ‘Q’ (<esc> will cancel the automation of the IE SaveAs dialog). The next time the crawl runs, it will resume where it left off. Crawling acts as if you were subscribed to my blog via RSS. Once you have all current content, Crawling again later will just add any new content. The saved content is the entire blog entry web page, including any comments. As an exercise, readers are encouraged to make the web crawling execute on a background thread! A crawl starts at the main page http://blogs.msdn.com/Calvin_Hsia , which shows any new content and has links on the side bar for any other posts. The page is loaded and then parsed for any links. Any links pointing to my

Webcrawl a Blog to Retrieve All Entries Locally

Embed Size (px)

DESCRIPTION

Jurnal - Chapter I part I : Prolog

Citation preview

Page 1: Webcrawl a Blog to Retrieve All Entries Locally

Webcrawl a blog to retrieve all entries locally: RSS on steroidsToday’s sample shows how to create a web crawler in the background. This crawler starts with a web page, looks for all links on that page, and follows all those links. The links are filtered to my blog, but generalizing the code to search the entire web or some other site is trivial (if you have enough disk space<g>). (VB.Net version to appear soon on this blog.) I was doing a search on my blog for “ancestors” via the Search box on the sidebar on the left, and there were no results. Strange, I thought, so I used MSN search for my site: http://search.msn.com/results.aspx?FORM=TOOLBR&q=ancestors+site%3Ahttp%3A%2F%2Fblogs.msdn.com%2FCalvin_Hsia%2F That search succeeded: it came up with the expected blog entry. This incident reminded me of the fact that I’ve done a lot of work to create my blog, but I depend on a 3rd party to maintain it. There are hundreds of code samples, with links to references. If the blog server were to disappear for some reason, so would all my content. I wanted to retrieve all my blog content into a local table. Then I can manipulate it any way I want. In particular, suppose I want to read my entire blog. I would have to do a lot of manual clicking to get to the month/day of the post, and then I might have missed something because I’m manually crawling. That’s pretty cumbersome. Also, I can have all of a blog available while offline, updating when connected. So I wrote a code sample below that crawls my blog, looking for all the blog posts, and shows them in a form which has search capability. Because it’s all local, searching and navigating from post to post is extremely fast. The entry is displayed in a web control, so the page looks just like it would online and the hyperlinks are all live.You can start a web crawl by pushing the Crawl button. You can interrupt the web crawl by typing ‘Q’ (<esc> will cancel the automation of the IE SaveAs dialog). The next time the crawl runs, it will resume where it left off. Crawling acts as if you were subscribed to my blog via RSS. Once you have all current content, Crawling again later will just add any new content. The saved content is the entire blog entry web page, including any comments.  As an exercise, readers are encouraged to make the web crawling execute on a background thread! A crawl starts at the main page http://blogs.msdn.com/Calvin_Hsia, which shows any new content and has links on the side bar for any other posts. The page is loaded and then parsed for any links. Any links pointing to my blog are inserted into a table if they’re not there already. Then the table is scanned for any unfollowed links and the process repeats. If a page is a leaf node (currently any link with 8 backslashes) then the Publication date is parsed, and the file is saved in the MHT field in the table. The link parsing was a little complicated due to some comment spam reducing measures and some broken links when the blog host server switched software. You will probably have to modify the code if you want to do the same for other blogs. For example, some blogs may have the Publication date in a different place. Others may have archive links elsewhere or in a different format. I experimented with using HTTPGet cTempFile=ADDBS(GETENV("TEMP"))+SYS(3)+".htm"LOCAL oHTTP as "winhttp.winhttprequest.5.1"LOCAL cHTMLoHTTP=NEWOBJECT("winhttp.winhttprequest.5.1")oHTTP.Open("GET","http://blogs.msdn.com/calvin_hsia/archive/2004/06/28/168054.aspx",.f.)oHTTP.Send()STRTOFILE(ohTTP.ResponseText,cTempFile)

Page 2: Webcrawl a Blog to Retrieve All Entries Locally

oIE=CREATEOBJECT("InternetExplorer.Application")oIE.Visible=1oIE.Navigate(cTempFile) But the content looked pretty bad, because of the CSS references, pictures, etc.  Being able to automate IE was helpful, but how do you parse the HTML for the links to each blog entry? I thought about using an XSLT, but that was fairly complex. I used the IE Document model IHTMLDocument,to search through the HTML nodes for links.  IE has a feature that saves a web page to a single file: Web Archive, single file(*.mht) from the File->SaveAs menu option. So I used Windows Scripting Host to automate this feature. Making the code run in a background thread is trivial: just use the ThreadClass from here. See also :            Wite your own RSS News/Blog aggregator in <100 lines of code

Use a simple XSLT to read the RSS feed from a blog, Do you like reading a blog author? Retrieve all blog entries locally for reading/searching using XML,

XSLT, XPATHGenerating VBScript to read a blog

    CLEAR ALLCLEAR #define WAIT_TIMEOUT                     258#define ERROR_ALREADY_EXISTS             183#define WM_USER                                                              0x400 SET EXCLUSIVE OFF SET SAFETY OFF SET ASSERTS ON PUBLIC oBlogForm as FormoBlogForm=creat("BlogForm","blogs.msdn.com/Calvin_Hsia")oBlogForm.Visible=1DEFINE CLASS BlogForm AS Form          Height=_screen.Height-80          Width = 900          AllowOutput=0          left=170          cBlogUrl=""          oThreadMgr=null          ADD OBJECT txtSearch as textbox WITH width=200          ADD OBJECT cmdSearch as CommandButton WITH left=210,caption="\<Search"          ADD OBJECT cmdCrawl as CommandButton WITH left=310,caption="\<Crawl"          ADD OBJECT cmdQuit as CommandButton WITH left=410,caption="\<Quit"          ADD OBJECT oGrid as Grid WITH ;                   width = thisform.Width,;                   top=20,;                   ReadOnly=1,;                   Anchor=15          ADD OBJECT oWeb as cWeb WITH ;                   top=230,;                   height=thisform.Height-250,;                   width = thisform.Width,;                   Anchor=15          ADD OBJECT lblStatus as label WITH top = thisform.Height-18,width = thisform.Width,anchor=4,caption=""          PROCEDURE Init(cUrl as String)                   this.cBlogUrl=cUrl                   IF !FILE("blogs.dbf")

Page 3: Webcrawl a Blog to Retrieve All Entries Locally

                             CREATE table Blogs(title c(250),pubdate t,link c(100),followed i, Stored t,mht m)                             INDEX on link TAG link                             INDEX on pubdate TAG pubdate DESCENDING                              INSERT INTO Blogs (link) VALUES (cUrl)     && jump start the table with a link                             INSERT INTO blogs (link) VALUES ('http://blogs.msdn.com/vsdata/archive/2004/03/18/92346.aspx')        && early blogs                             INSERT INTO blogs (link) VALUES ('http://blogs.msdn.com/vsdata/archive/2004/03/31/105159.aspx')                             INSERT INTO blogs (link) VALUES ('http://blogs.msdn.com/vsdata/archive/2004/04/05/107986.aspx')                             INSERT INTO blogs (link) VALUES ('http://blogs.msdn.com/vsdata/archive/2004/05/12/130612.aspx')                             INSERT INTO blogs (link) VALUES ('http://blogs.msdn.com/vsdata/archive/2004/06/16/157451.aspx')                   ENDIF                    USE blogs SHARED    && reopen shared                   this.RequeryData()                   this.RefrGrid          PROCEDURE RequeryData                   LOCAL cTxt, cWhere                   cTxt=ALLTRIM(thisform.txtSearch.value)                   cWhere= "!EMPTY(mht)"                   IF LEN(cTxt)>0                             cWhere=cWhere+" and ATC(cTxt, mht)>0"                   ENDIF                    SELECT * FROM blogs WHERE  &cWhere ORDER BY pubdate DESC INTO CURSOR Result                   thisform.lblStatus.caption="# records ="+TRANSFORM(_tally)                   WITH this.oGrid                             .RecordSource= "Result"                             .Column1.FontSize=14                             .Column1.Width=this.Width-120                             .RowHeight=25                   ENDWITH                    thisform.refrGrid                PROCEDURE RefrGrid                   cFilename=ADDBS(GETENV("temp"))+SYS(3)+".mht"                   STRTOFILE(mht,cFilename)                   thisform.oWeb.Navigate(cFilename)          PROCEDURE oGrid.AfterRowColChange(nColIndex as Integer)                   IF this.rowcolChange=1       && row changed                             thisform.RefrGrid                   ENDIF           PROCEDURE cmdQuit.Click                   thisform.Release          PROCEDURE cmdCrawl.Click                   thisform.txtSearch.value=""                   fBackgroundThread=.t.       && if you want to run on background thread                   IF this.Caption = "\<Crawl"                             thisform.lblStatus.caption= "Blog crawl start"                             CreateCrawlProc()                             IF fBackgroundThread                                      this.Caption="Stop \<Crawl"                                      *Get ThreadManager from http://blogs.msdn.com/calvin_hsia/archive/2006/05/23/605465.aspx                                      thisform.oThreadMgr=NEWOBJECT("ThreadManager","threads.prg")

Page 4: Webcrawl a Blog to Retrieve All Entries Locally

                                       thisform.oThreadMgr.CreateThread("MyThreadFunc",thisform.cBlogUrl,"oBlogForm.CrawlDone")                                      thisform.lblStatus.caption= "Background Crawl Thread Created"                             ELSE                                      LOCAL oBlogCrawl                                      oBlogCrawl=NEWOBJECT("BlogCrawl","MyThreadFunc.prg","",thisform.cBlogUrl)          && the class def resides in MyThreadFunc.prg                                      thisform.CrawlDone                             ENDIF                    ELSE                             this.Caption="\<Crawl"                             IF fBackgroundThread AND TYPE("thisform.oThreadMgr")="O"                                      thisform.lblStatus.caption= "Attempting thread stop"                                      thisform.oThreadMgr.SendMsgToStopThreads()                             ENDIF                    ENDIF           PROCEDURE CrawlDone                   thisform.oThreadMgr=null                    thisform.cmdCrawl.caption="\<Crawl"                   thisform.lblStatus.caption= "Crawl done"                   this.RequeryData()          PROCEDURE cmdSearch.Click                   thisform.RequeryData          PROCEDURE destroy                   IF USED("result")                             USE IN result                   ENDIF                    SELECT Blogs                   SET MESSAGE TO                    SET FILTER TO                    SET ORDER TO LINK   && LINKENDDEFINE DEFINE CLASS cweb as olecontrol          oleclass="shell.explorer.2"          PROCEDURE refreshxxx                   NODEFAULT          PROCEDURE TitleChange(cText as String)                   thisform.caption=cText          PROCEDURE Navigatecomplete2(pDisp AS VARIANT, URL AS VARIANT) AS VOID                   IF url=GETENV("TEMP")                             ERASE (url)                   ENDIF ENDDEFINE  PROCEDURE CreateCrawlProc as String      && Create the Thread proc, which includes the crawling classTEXT TO cstrVFPCode TEXTMERGE NOSHOW && generate the task to run: MyThreadFunc****************************************************************************************************          PROCEDURE MyThreadFunc(p2)      && p2 is the 2nd param to MyDoCmd                   TRY                             DECLARE integer GetCurrentThreadId in WIN32API                              DECLARE integer PostMessage IN WIN32API integer hWnd, integer nMsg, integer wParam, integer lParam                             cParm=SUBSTR(p2,AT(",",p2)+1)

Page 5: Webcrawl a Blog to Retrieve All Entries Locally

                             hWnd=INT(VAL(p2))                             oBlogCrawl=CREATEOBJECT("BlogCrawl",cParm)                   CATCH TO oEx                             DECLARE integer MessageBoxA IN WIN32API integer,string,string,integer                             MESSAGEBOXA(0,oEx.details+" "+oEx.message,TRANSFORM(oex.lineno),0)                   ENDTRY                   PostMessage(hWnd, WM_USER, 0, GetCurrentThreadId())DEFINE CLASS BlogCrawl as session          oWeb=0          oWSH=0          fStopCrawl=.f.          hEvent=0          cMonths="January   February  March     April     May       June      July      August    September October   November  December  "          cCurrentLink=""          PROCEDURE init(cBlogUrl)                   LOCAL fDone,nRec,nStart                   nStart=SECONDS()                   DECLARE integer CreateEvent IN WIN32API integer lpEventAttributes, integer bManualReset, integer bInitialState, string lpName                   DECLARE integer CloseHandle IN WIN32API integer                    DECLARE integer WaitForSingleObject IN WIN32API integer hHandle, integer dwMilliseconds                   DECLARE integer GetLastError IN WIN32API                    this.hEvent = CreateEvent(0,0,0,"VFPAbortThreadEvent") && Get the existing event                   IF this.hEvent = 0                             THROW "Creating event error:"+TRANSFORM(GetLastError())                   ENDIF                    ?"Start Crawl"                   DECLARE integer GetWindowText IN WIN32API integer, string @, integer                   DECLARE integer Sleep IN WIN32API integer                   this.oWeb=CREATEOBJECT("InternetExplorer.Application")                   this.oWeb.visible=1                   this.oweb.top=0                   this.oweb.left=0                   this.oweb.width=500                   this.oWSH=CREATEOBJECT("Wscript.Shell")                   USE blogs ORDER 1                   REPLACE link WITH cBlogUrl, followed WITH 0       && set flag to indicate this page needs to be retrieved and crawled                   this.fStopCrawl=.f.                   fDone = .f.                   DO WHILE !fDone AND NOT this.fStopCrawl                             fDone=.t.                             GO TOP                              SCAN WHILE NOT this.fStopCrawl                                      nRec=RECNO()                                      IF followed = 0                                                REPLACE followed WITH 1                                                this.BlogCrawl(ALLTRIM(link))                                                IF this.fStopCrawl                                                          GO nRec                                                          REPLACE followed WITH 0    && restore flag                                                ENDIF                                                 fDone = .f.                                      ENDIF

Page 6: Webcrawl a Blog to Retrieve All Entries Locally

                             ENDSCAN                   ENDDO                    ?"Done Crawl",SECONDS()-nStart          PROCEDURE BlogCrawl(cUrl)                   LOCAL fGotUrl,cTitle                   fGotUrl = .f.                   DO WHILE !fGotUrl    && loop until we've got the target url in IE with no Error                             this.oweb.navigate2(cUrl)                             DO WHILE this.oweb.ReadyState!=4                                      ?"Loading "+cUrl                                      Sleep(1000)   && yield processor                                      IF this.IsThreadAborted()                                                this.fStopCrawl=.t.                                                ?"Aborting Crawl"                                                RETURN                                       ENDIF                              ENDDO                              cTitle=SPACE(250)                             nLen=GetWindowText(this.oWeb.HWND,@cTitle,LEN(cTitle))                             cTitle=LEFT(cTitle,nLen)                             IF EMPTY(cTitle) OR UPPER(ALLTRIM(cTitle))="ERROR" OR ("http"$LOWER(cTitle) AND "400"$cTitle)                                       ?"Error retrieving ",cUrl," Retrying"                             ELSE                                      fGotUrl = .t.                             ENDIF                    ENDDO                    this.cCurrentLink=cUrl                   IF OCCURS("/",cUrl)=8          &&http://blogs.msdn.com/calvin_hsia/archive/2005/08/09/449347.aspx                             cMht=this.SaveAsMHT(cTitle) && save the page before we parse                             IF this.fStopCrawl                                      RETURN .f.                             ENDIF                              REPLACE title WITH STRTRAN(STRTRAN(cTitle," - Microsoft Internet Explorer",""),"Calvin Hsia's WebLog : ",""),;                                       mht WITH cMht,Stored WITH DATETIME()                             IF EMPTY(title)         && for some reason, the page wasn't retrieved                                      REPLACE followed WITH 0                             ENDIF                    ENDIF                    ?"Parsing HTML"                   this.ProcessNodes(this.oWeb.Document,0) && Recur through html nodes to find links                   ?"Done Parsing HTML"          PROCEDURE ProcessNodes(oNode,nLev)    && recursive routine to look through HTML                   LOCAL i,j,dt,cClass,oC,cLink                   IF this.IsThreadAborted() OR nLev > 30     && limit recursion levels                             RETURN                   ENDIF                    WITH oNode                             DO CASE                              CASE LOWER(.NodeName)="div"    && look for pub date                                      IF OCCURS("/",this.cCurrentLink)=8 && # of backslashes in blog leaf entry                                                oC=.Attributes.GetnamedItem("class")                                                IF !ISNULL(oC) AND !EMPTY(oC.Value)                                                          cClass=oC.Value

Page 7: Webcrawl a Blog to Retrieve All Entries Locally

                                                          IF cClass="postfoot" OR cClass = "posthead"                                                                   cText=ALLTRIM(STRTRAN(.innerText,"Published",""))                                                                   IF !EMPTY(cText)                                                                             dt=this.ToDateTime(cText)                                                                             IF SEEK(this.cCurrentLink,"blogs")                                                                                      REPLACE pubdate WITH dt                                                                             ELSE                                                                                      ASSERT .f.                                                                             ENDIF                                                                    ENDIF                                                           ENDIF                                                 ENDIF                                       ENDIF                              CASE .nodeName="A"                                      cLink=LOWER(STRTRAN(.Attributes("href").value,"%5f","_"))                                      IF ATC("http://blogs.msdn.com/calvin_hsia/",cLink)>0                                                IF ATC("#",cLink)=0 AND ATC("archive/2",cLink)>0                                                          *http://blogs.msdn.com/calvin_hsia/archive/2004/10/11/<a%20rel=                                                           IF "<"$cLink   && comment spam prevention: *http://blogs.msdn.com/calvin_hsia/archive/2004/10/11/240992.aspx*<a rel="nofollow" target="_new" href="<a rel="nofollow" target="_new" href="http://www.53dy.com ">http://www.53dy.com</a>                                                                                                                                                                         ELSE                                                                     *http://blogs.msdn.com/calvin_hsia/archive/2004/11/16/visual%20foxpro                                                                   IF "%"$cLink                                                                    *http://blogs.msdn.com/calvin_hsia/archive/2004/11/16/258422.aspx                                                                   * broken link: host updated software for category links                                                                   *<A title="Visual Foxpro" href="http://blogs.msdn.com/calvin_hsia/archive/2004/11/16/Visual%20Foxpro ">Visual Foxpro</A>*                                                                           SET STEP ON                                                                    ELSE                                                                              IF !SEEK(cLink,"Blogs")                                                                                      INSERT INTO Blogs (link) VALUES (cLink)                                                                             ENDIF                                                                    ENDIF                                                           ENDIF                                                 ENDIF                                       ENDIF                              ENDCASE                              FOR i = 0 TO .childNodes.length-1                                      this.ProcessNodes(.childNodes(i),nLev+1)                             ENDFOR                    ENDWITH                    PROCEDURE ToDateTime(cText as String) as Datetime                             *Friday, April 01, 2005 11:30 AM by Calvin_Hsia                              LOCAL dt as Datetime                             ASSERT GETWORDNUM(cText,6)$"AM PM"                             nHr = INT(VAL(GETWORDNUM(cText,5)))                             IF GETWORDNUM(cText,6)="PM" AND nhr < 12                                      nHr=nHr+12

Page 8: Webcrawl a Blog to Retrieve All Entries Locally

                             ENDIF                              dt=CTOT(GETWORDNUM(ctext,4) + "/" +; && Year                                      TRANSFORM(INT(1+(AT(GETWORDNUM(cText,2),this.cMonths)-1)/10)) + "/" +;          && month                                      TRANSFORM(VAL(GETWORDNUM(cText,3)))  + "T" +;     && day of month                                      TRANSFORM(nHr)+":"+;               && hour                                      TRANSFORM(VAL(SUBSTR(GETWORDNUM(cText,5),4))))  && minute                             ASSERT !EMPTY(dt)                   RETURN dt          PROCEDURE SaveAsMHT(cTitle as String) as String                   fRetry = .t.                   DO WHILE fRetry                             fRetry = .f.                             WITH this.oWSH                                      .AppActivate(cTitle)   && bring IE to the foreground                                      TEMPFILEMHT= "c:\t.mht"    && temp file can be constant                                      ERASE (TEMPFILEMHT)                                      .SendKeys("%fa"+TEMPFILEMHT+"{tab}w%s")    && Alt-F (File Menu) S (Save As) type Web Archive  Alt-S                                      nTries=5                                      DO WHILE !FILE(TEMPFILEMHT)      && wait til IE saves the file                                                Sleep(5000)                                                nTries=nTries-1                                                IF nTries=0                                                          fRetry=.t.                                                          EXIT                                                 ENDIF                                                 IF this.IsThreadAborted()                                                          this.fStopCrawl=.t.                                                          ?"Aborting crawl"                                                          RETURN ""                                                ENDIF                                       ENDDO                                       sleep(100)                             ENDWITH                    ENDDO                    RETURN FILETOSTR(TEMPFILEMHT)          RETURN          PROCEDURE IsThreadAborted as Boolean                   IF WaitForSingleObject(this.hEvent,0) = WAIT_TIMEOUT                             RETURN .f.                   ENDIF                    RETURN .t.          PROCEDURE destroy                   this.oWeb.visible=.f.                   this.oWeb=0                   CloseHandle(this.hEvent)ENDDEFINE****************************************************************************************************ENDTEXT          STRTOFILE(cstrVFPCode,"MyThreadFunc.prg")          COMPILE MyThreadFunc.prg*SELECT PADR(TRANSFORM(YEAR(pubdate)),4)+"/"+PADL(MONTH(pubdate),2,"0") as mon,COUNT(*) FROM blogs WHERE !EMPTY(mht) GROUP BY mon ORDER BY mon DESC INTO CURSOR result 

Page 9: Webcrawl a Blog to Retrieve All Entries Locally

RETURNPublished Thursday, May 25, 2006 5:30 PM by Calvin_Hsia Filed under: Visual FoxPro, Web

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# re: Webcrawl a blog to retrieve all entries locally: RSS on steroids

Friday, May 26, 2006 3:46 PM by Kenneth Tamayo Calvin,

That's really beautiful code... Excellent pointers for diverse applications deployed with Visual FoxPro.  Thanks for commenting/posting it!!

# re: Webcrawl a blog to retrieve all entries locally: RSS on steroids

Tuesday, May 30, 2006 9:02 AM by SednaY I've updated the VFP MT example based on this (at http://codegallery.gotdotnet.com/SednaY) to be sort of like .NET: Example Use: * t=CREATEOBJECT('testserver.thread') * t.start(5,"do c:\MTmyVFP\MyThreadFunc WITH p2") * && start method params:(1)#threads,(2)VFP code to MT,(3)Silent mode * ?t.check && returns .T. if completed * t=null && cleanup Simple, fast, efficient - it is VFP!!

# Use conditional build events to freshen zip files in Visual Studio

Tuesday, June 06, 2006 1:06 PM by Calvin Hsia's WebLog I wanted to update a couple zip files of the VB version of my Blog Crawler (to be posted soon) with the...

# The VB version of the Blog Crawler

Monday, June 12, 2006 8:26 AM by Calvin Hsia's WebLog This is the VB.Net 2005 version of the Blog Crawler. It’s based on the Foxpro version, but.it uses SQL...

# How does EventHandler work? IConnectionPoint!

Wednesday, June 14, 2006 7:53 PM by Calvin Hsia's WebLog

Page 10: Webcrawl a Blog to Retrieve All Entries Locally

The EventHandler function allows you to connect some code to an object’s event interface. For example,...

# How do I turn off the User Interface in an unattended application?

Wednesday, July 05, 2006 4:45 PM by Calvin Hsia's WebLog Sometimes you run a program and you don’t want it to show any dialogs or User Interface at all. For example,...

# Calvin's got another cool utility

Tuesday, July 11, 2006 8:36 PM by yag: Community and Architecture Calvin has written a blog crawler with both VFP and VB.NET versions that allows you to back up your own...

# [email protected]

Saturday, July 22, 2006 9:45 PM by [email protected] funny ringtones

# deciacco.com blog &raquo; GetHtml - Foxpro App

Friday, December 15, 2006 6:40 PM by deciacco.com blog » GetHtml - Foxpro App

PingBack from http://deciacco.com/blog/archives/12

# re: Webcrawl a blog to retrieve all entries locally: RSS on steroids

Wednesday, January 31, 2007 9:00 PM by ClaudeFox

I've updated this VFP Web Crawler to more closely match the VB.Net version.  Check it out at:

http://www.codeplex.com/vfpwebcrawler

All source is included...

# Install Northwind for SQL Express and use Visual Studio and DLINQ to query it

Friday, August 17, 2007 9:03 PM by Calvin Hsia's WebLog

SQLExpress is free and comes with Visual Studio, but the sample Northwind database isn’t included. You

# MSDN Blog Postings &raquo; Install Northwind for SQL Express and use Visual Studio and DLINQ to query it

Friday, August 17, 2007 10:00 PM by MSDN Blog Postings » Install Northwind for SQL Express and use Visual Studio and DLINQ to query it

Page 11: Webcrawl a Blog to Retrieve All Entries Locally

PingBack from http://msdnrss.thecoderblogs.com/2007/08/17/install-northwind-for-sql-express-and-use-visual-studio-and-dlinq-to-query-it/

# Install Northwind for SQL Express and use Visual Studio and DLINQ to query it

Friday, August 17, 2007 10:05 PM by Noticias externas

SQLExpress is free and comes with Visual Studio, but the sample Northwind database isn’t included. You

# re: Webcrawl a blog to retrieve all entries locally: RSS on steroids

Friday, August 24, 2007 9:06 AM by SednaY

I updated a version of this code to include an easy to use VFP project and

-ability to specify number of threads

-better switching between blogs

-debug option to make crawling visible

See VFPWebCrawler 2.0 at:

http://www.codeplex.com/VFPWebcrawler

# Customer site visit: 3-D printer company

Sunday, November 11, 2007 12:03 AM by Calvin Hsia's WebLog

I spent a few hours at a local company called 2Bot ( http://www.2bot.com/ ) which makes a 3-D printer

# Internet Explorer History &raquo; Calvin Hsia&#8217;s WebLog : Webcrawl a blog to retrieve all entries locally &#8230;

Saturday, December 29, 2007 7:06 AM by Internet Explorer History » Calvin Hsia’s WebLog : Webcrawl a blog to retrieve all entries locally …

PingBack from http://internet-explorer-history.blogyblog.info/?p=1873

# Actors and Actresses &raquo; Archive du blog &raquo; Calvin Hsia&#8217;s WebLog : Webcrawl a blog to retrieve all entries locally &#8230;

Friday, January 04, 2008 1:26 PM by Actors and Actresses » Archive du blog » Calvin Hsia’s WebLog : Webcrawl a blog to retrieve all entries locally …

PingBack from http://actors.247blogging.info/?p=3677

# http://beta.blogs.msdn.com/calvin_hsia/archive/2006/05/25/607588.aspx

Page 12: Webcrawl a Blog to Retrieve All Entries Locally

Wednesday, March 26, 2008 4:55 AM by http://beta.blogs.msdn.com/calvin_hsia/archive/2006/05/25/607588.aspx

PingBack from http://frankthefrank.info/entry.php?id=kwws%3d22ehwd1eorjv1pvgq1frp2fdoylqbkvld2dufklyh25339238258293%3a8%3b%3b1dvs%7b

# How to interrupt your code

Thursday, May 15, 2008 10:44 PM by Calvin Hsia's WebLog

I received a question: Simply, is there a way of interrupting a vfp sql query once it has started short

# Calvin Hsia s WebLog Webcrawl a blog to retrieve all entries locally | Paid Surveys

Friday, May 29, 2009 1:15 PM by Calvin Hsia s WebLog Webcrawl a blog to retrieve all entries locally | Paid Surveys

PingBack from http://paidsurveyshub.info/story.php?title=calvin-hsia-s-weblog-webcrawl-a-blog-to-retrieve-all-entries-locally

# Calvin Hsia s WebLog Webcrawl a blog to retrieve all entries locally | Insomnia Cure

Monday, June 08, 2009 5:59 PM by Calvin Hsia s WebLog Webcrawl a blog to retrieve all entries locally | Insomnia Cure

PingBack from http://insomniacuresite.info/story.php?id=11048

http://download.codeplex.com/Project/Download/FileDownload.aspx?ProjectName=VFPX&DownloadId=69408&FileTime=128877569812070000&Build=15321

Run your application forms on the web

I received a customer question:

 

I have looked all over the web and still searching, and found your blog. I have a very specific issue and I need to ask whether this is doable first of all and how to do it after.

 

First of all, I need to make a foxPro app that wll also run as a web application. Can this be done? And if so how? I have looked into creating an ASP.NET page and calling a COM from there. However, is this equivalent to calling the actual .exe, is it like running the program from within

Page 13: Webcrawl a Blog to Retrieve All Entries Locally

ASP.NET or is it simply calling some methods and functionality? Basically, can foxpro be used as a web DB/app?

 

Can you give me some guidelines on how to go about this? Or direct me to some additional info?

 

According to my Ship It award (an award given to employees for shipping a Microsoft product), Visual FoxPro 5.0 shipped in August 96. VFP5 ships with a sample called FoxIsapi, which I created (at the prompting of Melissa Dunn) to demonstrate creating a Fox Form in the Fox form designer, showing Employee information, their photos and their sales in a grid.

 

For further information about FoxIsapi and how it worked, see this article: http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarfoxgen/html/msdn_custole.asp

Quote from article:

the same form can be deployed in four different ways: as a normal Visual FoxPro form, as a Visual FoxPro run time, as an OLE Automation server from an OLE Automation client, and over the Internet using any Web browser!

Over a decade ago, I used to demonstrate this sample, changing the FoxIsapi form in the designer, saving it, and showing how easily that one change propagates to all 4 deployment scenarios.

 

I've copied/pasted the article below because it's marked Archive Content, and might be deleted any day..

 

FoxIsapi is a sample using ISAPI extensions, just as ASP pages do.

 

Designing an application that can run on the web and as a rich Windows application requires careful architecture. You'll need to separate the application into multiple tiers: you might have heard of presentation layer (the User Interface), the business rules layer, the data layer. You’ll have to deal with saving application state to know that a particular user is, for example, editing record #5 when hitting Save, while another might be editing record #4. FoxIsapi uses a cookie to save application state.

 

My personal digital photo/video collection  application runs on both the web and as a rich windows client.  I can query for any photos just with a string search, including from my cell phone My rich windows client can page through dozens of photos at a time, and Enable crop and zooming in on your digital photograph display form

It’s actually 2 separate applications talking to the same data, sharing some code.

 

Page 14: Webcrawl a Blog to Retrieve All Entries Locally

 

Try viewing the FoxIsapi code (samples->servers->Foxisapi->FoxIs.pjx)

Try running the code (this shows the form in 2 ways: as a Fox Form and in the browser)

 

SET SAFETY OFF

CD HOME(2)+"\servers\foxisapi\foxis"

PUBLIC x

x=NEWOBJECT("employee","employee")

x.Visible=1

?x.startup()      && shows the generated HTML

STRTOFILE(x.startup(),"d:\t.htm")

! d:\t.htm

 

 

You might have to Alt-Tab to the form because it’s a top-level window.

 

There are a lot more wrinkles to getting the web form to work under IIS.

You’ll have to set the employee.vcx cdatapath property to point to the data (change the cgraphicspath while you’re at it), build the project as an MTDLL, then see if this code works:

x=CREATEOBJECT("foxis.employee")

?x.startup()      && shows the generated HTML

 

These paths are currently like HOME(1)+"\Samples\Data"), and HOME(1)  (the VFP install directory) in a COM server has a different value

You’ll have to deal with IIS security. On Windows XP (after you install IIS), copy tools\foxisapi\foxisapi.dll to c:\inetpub\wwwroot\scripts\foxisapi.dll (You might need to make the scripts directory)

Disable anonymous access for now, and use “Integrated Windows Authentication” (Control Panel->Admin Tools->IIS->Web Sites->Default Web Site->Right click on  Scripts->Directory Security->Anon Access and Authentication

On the Directory tab of the same dialog, change “Execute Permissions” to “Scripts and Executables”.

Try this http://localhost/scripts/foxisapi.dll in a browser. If you get an error message that FoxIsapi failed, it means you’re doing well. Foxisapi.dll loaded and working. If you get a dialog asking what you want to do with foxisapi.dll, then you don’t have execute permissions working.

Open a browser, this URL: http://localhost/scripts/foxisapi.dll/foxis.employee.startup will display the form, with the grid and photo. Try View->Source to see that the HTML is that from the Startup method.

Page 15: Webcrawl a Blog to Retrieve All Entries Locally

 

Hints:

You can stop/start IIS with “Net stop w3svc”, “Net start w3svc”, or IISRESET On Windows Server 2008 (Vista Server) I installed VFP to a folder “d:\VFP” because Program

Files folder is locked down for security reasons. Start VFP as an Admin because Admin rights are required to register a COM server in the registry. In Server Manager, select Roles->Web Server (IIS) to see what Role Services are installed. Install Application development, which includes ISAPI Extensions. You'll also need to go to Scripts->Handler Mappings and edit the script map for ISAPI-dll, and point it to scripts\Foxisapi.dll

See the source code for foxisapi for more info (Samples\Servers\Foxisapi\CSource\Foxisapi.cpp)

Using ‘Integrated Windows Authentication” means the web request will come in as the same user that started the browser. That means if you have rights to read/write the files/data, then through the browser you'll have the same rights. Allowing anonymous access means you'll have to grant the anonymous user rights

Try this http://localhost/scripts/foxisapi.dll/status Try this http://localhost/scripts/foxisapi.dll/reset Try creating FOXISAPI.INI file with these 2 lines in the scripts dir (will be read upon reset)

[foxisapi]

busytimeout=4

 

 

 

User designed forms, like Employee.scx, inherit from the Isapi.Isform class, which has a GenHTML method. This method loops through all the controls contained on the form and generates HTML for them.

 

Keep in mind FoxIsapi is just a sample, from almost a dozen years ago, when the internet was very young. Newer web architectures abound.

 

For an article on using ASP to serve up web pages with Fox (which doesn’t require shutting down the server), see Blogs get 300 hits per hour: Visual FoxPro can count.

 

You can also run foxis.employee out of process using COM+ applications: Control Panel->Admin Tools->Component Services->Computer->My Computer->COM+ Applications. Create a New empty application called FOXIS, then under FOXIS->Components, Install New Component, and navigate to the MTDLL you built:  foxis.dll. Now when you instantiate foxis.employee, the ball will spin and it will be out of the IIS process (inetinfo.exe).

 

Also, there are numerous Fox web applications using Rick Strahl's Web Connection (www.west-wind.com)

 

Page 16: Webcrawl a Blog to Retrieve All Entries Locally

Note: because this is an old sample, there is no guarantee of support if it doesn't work for you. I did get it to work on Windows Server 2008 and XP.

 

 

 

Visual FoxPro Technical Articles

 

Custom OLE Servers

Calvin HsiaMicrosoft Corporation

October 1996

Introduction

Think back to the early computer systems available only ten or twenty years ago. In these early days of computing, users could only run a single application at a time. There was no concept of multitasking. Sharing information between applications was very cumbersome. If one application processed the output of another application, then some sort of information sharing strategy had to be devised. Along came multitasking environments, such as Microsoft® Windows®, that allowed multiple applications to run simultaneously. Users felt more powerful because they could run a spreadsheet and their accounting program simultaneously, perhaps cutting/pasting data from one to the other. Sharing data between applications became easier.

As computer operating systems evolved, OLE 1.0 was introduced, in which users could actually "embed" data objects created by other applications right into their main application. When a user opens a Word document and double-clicks on the embedded Microsoft Excel object, Excel starts up and allows the user to interact with the data. However, this implementation meant that Excel would start as its own separate application, and the user would see both Excel and Word on the screen at the same time. Furthermore, the Word document’s representation of the Excel spreadsheet would be marked with hash marks, indicating that it was being edited by Excel via OLE. This early attempt at allowing users to create "compound documents" was effective, but ungainly.

Along came OLE 2.0, which allows OLE In-Place Activation. This made the Excel embedded object come alive in the same Application Window as the Word object, with the menus and toolbars being "merged." The concept of the "compound document" is being further propagated because the document has a single "merged" editing environment.

These evolutionary steps in compound document architecture really help the interactive user to be more productive with the operating environment. Users can run multiple applications simultaneously and hence realize productivity gains. However, application developers also have seen an evolution in application programmability, allowing application writers the ability to programmatically control other applications.

With Windows, a technology called DDE, or Dynamic Data Exchange was introduced. This allowed programmers to write programs in a particular programming language that could communicate and command some other applications to do their bidding. This concept of a programmer writing a Controller or Client that uses the services of a Server was cumbersome with DDE. The programmer would have to know the programming languages of both objects, would have to create a DDE callback routine, and would have to have intimate knowledge of the server applications.

Another feature of OLE 2, OLE Automation, addresses the shortcomings of DDE and introduces a new level of inter-application communication. OLE Automation allows the application developer to allow other applications to "talk" to it programmatically in a standard way. The programmer designs an "interface” through which an OLE Automation controller can invoke routines and change properties on the OLE Automation server.

An application that can be an OLE Automation Server multiplies its usefulness to the modern computer environment. A standalone application like Excel has many graphing, mathematical, statistical, and financial functions. Other applications can take advantage of these functions simply by programmatically calling Excel. The end user of the application doesn't even need to know that Excel is being used.

Page 17: Webcrawl a Blog to Retrieve All Entries Locally

Of course, in order to use an OLE server object, an application must be able to talk to it as an OLE Automation controller. Visual FoxPro™ version 3.x, Microsoft Excel, and Visual Basic® are all OLE Automation controller capable.

Calling Other Automation Objects

Several examples follow using Visual FoxPro as an OLE Automation controller. Most of these will work with Visual FoxPro 3.0 and Visual FoxPro 5.0.

An example using Microsoft ExcelPUBLIC oxox=CreateObject("Excel.application")ox.visible=.t.ox.windowstate=1ox.left = 10ox.application.top = 10ox.workbooks.addFOR i = 1 TO 3    FOR j= 1 to 5        ox.cells[i,j].value = I * 10 + j    ENDFORENDFOR

An example using Microsoft Wordox=CreateObject("word.basic")ox.appshowox.filenewox.insert("test")ox.filesaveas("c:\t.txt")

An example using MAPI

If you're feeling lonely, you can send mail to yourself using this code:

ox= createobject("mapi.session")?ox.logon("calvinh")?ox.name,ox.class,ox.operatingsystem,ox.versionmsg = ox.outbox.messages.addmsg.subject = "testsubject"msg.text = "Don't be lonely"recip = msg.recipients.addrecip.name = "calvinh"?recip.resolvemsg.send(.t.,.f.)?ox.logoff()

You can also create automatic message readers and generators via OLE Automation, and you can publish mail messages and folders over the intranet so that a client using a Web browser can view them.

An example using the Internetox=CreateObject("InternetExplorer.Application")ox.visible=.t.ox.navigate("http://www.msn.com")

If you don't like Internet Explorer or Netscape Browser, create your own in a Visual FoxPro form with just a few lines of code:

PUBLIC ox ox = CreateObject("MyInternetExplorer")

Page 18: Webcrawl a Blog to Retrieve All Entries Locally

ox.visible = .t.ox.oWeb.navigate(GETENV("computername"))  && insert your URL here  DEFINE CLASS MyInternetExplorer AS form    ADD OBJECT oWeb AS CWeb    caption = "My Internet Explorer"    PROCEDURE init        THISFORM.LEFT = 0        THISFORM.WIDTH = SYSMETRIC(1) * 3/4        THISFORM.height = SYSMETRIC(2)*3/4        THIS.Resize    PROCEDURE resize        This.oWeb.resize    ENDDEFINE DEFINE CLASS CWeb AS olecontrol    oleclass = "Shell.Explorer.1"    PROCEDURE Resize        THIS.Width = THISFORM.Width - 10        THIS.Height = THISFORM.Height - 10    PROCEDURE Refresh        nodefaultENDDEFINE

Then you can just click on any hyperlinks and get lost on the Web. Or you can type in a command:

ox.oweb.navigate("www.msn.com")

to go to a particular URL.

Calling Visual FoxPro from Another Application

Visual FoxPro 5.0 itself is an OLE automation server. This means that from any other application that can be an OLE automation controller, you can CreateObject("VisualFoxPro.application") to get an object reference to Visual FoxPro 5.0, and change properties and invoke methods.

From Another Application that Is Not OLE Automation Controller Capable

Some applications are not capable of being an OLE automation controller, and thus are incapable of using Visual FoxPro as an OLE automation server. However, the capability to call into the Visual FoxPro automation server is in a DLL called fpole.dll, which ships with Visual FoxPro 5.0. If these applications can call 32-bit DLLs, and then they can load this DLL and control Visual FoxPro. For example, a Windows Help file or Microsoft Word can declare and use DLLs and thus use Visual FoxPro as an automation server.

As an example of using this DLL, here's some code that you can run from Visual FoxPro 3.0 (even though Visual FoxPro 3.0 is an automation controller, it also can call into 32-bit DLLs) that calls Visual FoxPro 5.0 as an automation server.

From Visual FoxPro 3.0MYDLL = "fpole.dll"clearDECLARE integer SetOleObject in (MYDLL) stringDECLARE integer FoxDoCmd in (MYDLL) string,string DECLARE integer FoxEval in (MYDLL) string, string @,integerbuff="this is the initial value of buffer"+space(100)*=SetOleObject("visualfoxpro.application")

Page 19: Webcrawl a Blog to Retrieve All Entries Locally

i = 1do while !chrsaw()     mt = "?'FoxDoCmd" +str(i)+"'"    =FoxDoCmd(mt,"")    ?mt    i = i + 1enddo=inkey(.1)clea dlls

From Microsoft Word

From Microsoft Word, you can create a WordBasic program that starts Visual FoxPro 5.0 as an automation server, opens a database, and retrieves a specific record:

Declare Sub FoxDoCmd Lib "fpole.dll"(cCommand As String, cBringToFront As String) Sub mainFoxDoCmd "USE c:\fox40\samples\data\customer", ""FoxDoCmd "LOCATE FOR cust_id = 'CENTC'", ""FoxDoCmd "_CLIPTEXT = customer.company + CHR(9) + customer.contact", ""EditPasteEnd Sub

From a Windows Help file1.                  In the [CONFIG] section of a help project, register the .dll:

2.                      RegisterRoutine("fpole.dll","FoxDoCmd","SS") 3.                  In your .rtf file, call the function the same way you would call a native Help macro:

4.                      HotSpotText!FoxDoCmd("DO (HOME() + 'myprg')","a") 5.                  Compile and run the Help file.

You can see a sample of this in the main Visual FoxPro Help file. From the Visual FoxPro Help menu choose Sample Applications. When the Sample Applications topic appears, click on the RUN or OPEN buttons. This invokes fpole.dll to start Visual FoxPro and runs the program hlp2fox.prg in the main Visual FoxPro directory.

From Visual Test

From Visual Test, you can write a Visual Test Basic Script that calls into Visual FoxPro to get object references and mnemonic names of Visual FoxPro objects.

Viewport Clear DECLARE FUNCTION MessageBox LIB "user32.dll" ALIAS "MessageBoxA" (h&,m$,c$,f&) as long DECLARE FUNCTION FoxDoCmd cdecl LIB " fpole.dll" (h$,j$) as long DECLARE FUNCTION FoxEval cdecl LIB " fpole.dll" (h$,j$, leng&) as long  sub DoClick (x&,y&)    dim buff$,cmd$,flag%    flag% = 0    buff$ =  string$(132,32)    cmd$ = "oref = sys(1270," + str$(x&) + "," + str$(y&)+")"    'print cmd$    FoxDocmd(cmd$,"")    IF DoEval$("type('oref')") = "O" then        IF DoEval$("oref.name") <> "Screen" then

Page 20: Webcrawl a Blog to Retrieve All Entries Locally

            FoxDoCmd("kk = sys(1272,oref)","")            IF instr(DoEval$("kk"),".") > 0 then                FoxDoCmd("kk2 = LEFT(kk,AT('.',kk)-1)","")                FoxDoCmd("AINSTANCE(mm,kk2)","")                IF DoEval$("IIF(ALEN(mm,1) > 0,'A','B')") = "A" THEN                'IF DoEval$("AINSTANCE(mm,kk2) > 0") THEN                    FoxDoCmd("kk = mm[1] + SUBSTR(kk,AT('.',kk))","")                    cmd$ = DoEval("kk") +".click"                    print #1,cmd$                    print Cmd$                    FoxDoCmd(cmd$,"")                ENDIF            endif            flag% = 1        ENDIF    endif    IF flag% = 0 then        'Play "{Click 723, 356, Left}"        cmd$ = "{Click " + str$(x&) + "," + str$(y&)+", Left}"        print #1,"play " + CHR$(34) + cmd$ + chr$(34)        play cmd$    ENDIFend sub function DoEval$ ( s$)    dim buff$,cmd$,k&    buff$ =  string$(132,32)    k& = FoxEval(s$,buff$,len(buff$))    if k& <= 0 then        MessageBox(0,buff$,"Eval error",0)    endif    DoEval = buff$end function

The Visual FoxPro Help file has more information on how to use fpole.dll.

In-Process and Out-of-Process Servers

An in-process OLE server is simply a DLL that lives in the same process address space as the OLE client. An out-of-process server lives in its own process space, and thus is typically an .exe.

An in-process server is faster than an EXE server when passing parameters back and forth, but it also can crash the host if it crashes. There is very little protection from an errant DLL server for the host. An EXE server can crash and not necessarily crash the host. Also, an EXE server can be run on a remote machine using remote automation or COM in a distributed environment (DCOM).

Because an EXE server runs in its own process, the processor gives it processor time slices to execute whatever it wants, which is typically an event loop. A DLL server depends on the OLE client to give it process time. This implies that when an EXE server presents a user interface (UI) element, such as a form, to the user, the user can navigate the form as expected. The DLL server, however, can present the form, but cannot interact with it.

Generally, this is not a problem because OLE servers are typically designed to perform some sort of server task, which has no UI elements. For example, running an OLE server that serves Web pages typically means the server lives on an unattended Web server machine.

In-Process and Out-of-Process Servers in VFP 6

The new apartment model threading support requires that in-process .dll Automation servers not have user-interfaces. In Visual FoxPro 5.0, one could create (although it was not recommended) an in-process .dll Automation server that had a user-interface such as a form. The form could be used only for display because

Page 21: Webcrawl a Blog to Retrieve All Entries Locally

the form events are not supported. In Visual FoxPro 6.0, any attempts to create a user-interface in an in-process .dll Automation generates an error.

An out-of-process .exe Automation server can have a user-interface. A new Visual FoxPro 6.0 function, SYS(2335), has been added so you can disable modal events for an out-of-process .exe Automation server, which may be deployed remotely without intervention from a user. Modal events are created by user-defined modal forms, system dialogs, the MESSAGEBOX( ) function and the WAIT command, and so on.

Your First Custom OLE Automation Object

Create a new Visual FoxPro 5.0 project called FIRST. Add a new file first.prg with the following lines:

DEFINE CLASS myfirst AS custom OLEPUBLIC    PROCEDURE INIT        MessageBox(program(),"Here in my first OLE server")        ENDDEFINE

Note that there is nothing about this program that's new except the "OLEPUBLIC" keyword. (You can also make Visual Classes OLEPUBLIC from the Class Info dialog box.) What does this do? Absolutely nothing, until you Build an .exe or .dll from the project. If you watch carefully during the build process, you'll see the message "Creating Type Library and Registering OLE Server."

The build process creates a first.tlb, first.vbr, along with your OLE Server (either first.exe or first.dll). The TLB is an OLE Typelibrary, which can be browsed using any OLE Type Library browser, such as the ones that come with the Visual FoxPro Class Browser, Microsoft Excel, Visual Basic, and Visual C++®. The .VBR file is a text file of registry entries. These are almost identical to those that were created and entered into your registry. The only difference is that file names are prepended with their full path names. This .VBR file is useful for the Setup wizard and when you want to deploy your OLE server on another machine, which might use different paths.

Now try the following from the Command window:

    ox = CreateObject("first.myfirst")    *wait a few secs till the server comes up with the msgbox    * (you might have to alt-tab to the msgbox)    ?ox.application.name    ?ox.application.visible    ?ox.    ?ox.application.docmd("messagebox(home())")    ?ox.application.docmd("_cliptext=home() + sys(2003) + sys(2004)")

You can add the line OX = CreateObject("myfirst") to the PRG as the first line, and then the .exe file will run just like any normal .exe. (Note: the string inside this CreateObject is the Visual FoxPro class name and not an OLE ProgID.) If it were a visual class library, you can add a Main program that does a SET CLASSLIB to the appropriate VCX and then CreateObject the object (not the OLE ProgID).

Determining the Initial Directory

Note the last line in the sample. On my machine, it copies "C:\WINNT\SYSTEM32" to the Clipboard three times. This means that even though the OLE server is in a particular directory, OLE starts it with the WINNT\SYSTEM32 directory. This brings up an interesting problem. How does the OLE server find its component pieces, such as .DBFs, reports, and so on?

We need to determine the directory in which the OLE Server lives, because that's typically where the parts are. Then we can SET DEFAULT TO that directory, or we can SET PATH TO it. One solution is to make the INIT or LOAD method of the OLE Public class change to the right directory, with the directory name either hard coded or as a custom property on the class. This works fine on one machine, until the server is moved or installed on a different machine with a different path.

Fortunately, there are a couple WIN32 API functions available to help. For an EXE server, we can call the GetModuleFileName( ) function, which returns the full path name to the main .exe of the current process if null is passed as the first parameter. For a DLL server, we use the GetModuleHandle( ) function with the

Page 22: Webcrawl a Blog to Retrieve All Entries Locally

name of the DLL as the parameter to retrieve a handle to the server. We pass this handle to the GetModuleFileName( ) function to get the full path name of the DLL.

Note the use of the _VFP.StartMode property. The Help file shows the various values of this property, which indicate in which mode Visual FoxPro was started: normal, as an OLE server, or as a custom DLL or EXE OLE server:

CLOSE DATA ALLSET TALK OFFSET SAFETY OFFSET EXCLUSIVE OFFSET DELETED ONDECLARE INTEGER GetPrivateProfileString in win32api String,String,String,;      String @, integer,stringif _vfp.startmode>1    &&  automation server    LOCAL buf,nlen    buf=space(400)    DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer    * As an EXE or DLL, we need to find the home directory, not the curdir() and not    * the dir of the runtime. The classlibrary property shows this for PRGs, but not VCXs    IF _vfp.startmode = 3 && inproc dll        DECLARE INTEGER GetModuleHandle in win32api String        nlen=Getmodulefilename(GetModuleHandle(this.srvname+".dll"),@buf,len(buf))    ELSE        nlen=Getmodulefilename(0,@buf,len(buf))    ENDIF    buf = LEFTC(buf,nlen)    this.cdir = LEFTC(buf,RATC('\',buf)-1)    this.csrvname = SUBSTRC(buf,RATC('\',buf)+1)    this.csrvname = LEFTC(this.csrvname,AT_C(".",this.csrvname)-1)endif IF !EMPTY(this.cDir)    SET DEFAULT TO (this.cDir)ENDIF  SET PATH TO (this.cDatapath)use employee

Add the SAMPLES\DATA\TESTDATA.DBC to your FIRST.PJX. Add a new class to the project called MYFORM based on FORM and store it in first.vcx. Make the class OLE Public in the Class Info dialog box, and add three custom properties: cDir, cSrvName, and cDataPath. Initialize cDatapath to the directory where your testdata.dbc is, and cSrvName to FIRST. The cDir property will be initialized with the above code added to the LOAD event of the form class. Drag a couple of EMPLOYEE fields from the Project onto the class. Add the VCR class from the SAMPLES\CLASSES directory to your project, and drag an instance of the VCR button onto your form. Save the class and BUILD EXE first FROM first.

Now you've built an OLE server with the employee form. This server can be instantiated from any other OLE controller, but let’s test it from Visual FoxPro because it’s easy and familiar. In the Command window, type OX = CreateObject("myfirst.myclass"), then type OX.Application.visible=.t. This will make the Visual FoxPro run-time frame visible. OX.SHOW will make your form visible, and you can navigate around the form with the VCR buttons.

RELEASE OX or CLEAR ALL will release the server. If you had changed the ShowWindow property to 2, then the server form would be a Top Level form and the run-time Visual FoxPro desktop would not need to be shown. You can also add the line THISFORM.VISIBLE = .T. in the INIT event of the form class.

Now try BUILD DLL rather than BUILD EXE, and experiment. Note that you cannot interact with the server: when you bring the mouse over the server, you just get an hourglass. If the DLL server had the ability to interact with the user, then it would be more like an OLE or ActiveX Control. Even though you can't interact with the form,

Page 23: Webcrawl a Blog to Retrieve All Entries Locally

you can still give it commands from the Client: ox.application.docmd("skip") and ox.refresh will skip to the next record. ?ox.application.eval("recno()") will return the current record number, as expected.

Registry Issues

Below is my sample .vbr file. You'll notice the presence of long strings of numbers. These are GUIDs or Globally Unique Identifiers (sometimes called UUIDs, or Universally Unique Identifiers). These are automatically generated by your computer and are guaranteed to be virtually unique, based upon the time stamp and your machine's network card, among other things.

When an OLE client application does a CreateObject, the parameter is called the ProgID or Programmatic ID. The registry is searched for the ProcID to find the ClassID, which is just a GUID. Then that ClassID is located in the registry to find more information about the server.

Visual FoxPro Custom OLE servers are self-registering. To register an EXE server, just run the .exe with the /regserver option. The /unregserver unregisters the server. For a DLL server, run the utility Regsvr32.exe with the name of the DLL as the first parameter. This will register the DLL. To unregister it, add a second parameter.

Visual FoxPro OLE automation servers require the presence of the Visual FoxPro run time on the target machine. This is vfp500.dll and vfp5enu.dll (for the English U.S. platform).

VB4SERVERINFO HKEY_CLASSES_ROOT\first.myfirst = myfirstHKEY_CLASSES_ROOT\first.myfirst\NotInsertableHKEY_CLASSES_ROOT\first.myfirst\CLSID = {3DB63101-0FED-11D0-9A44-0080C70FB085}HKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085} = myfirstHKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\ProgId = first.myfirstHKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\VersionIndependentProgId = first.myfirstHKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\LocalServer32 = first.exe /automationHKEY_CLASSES_ROOT\CLSID\{3DB63101-0FED-11D0-9A44-0080C70FB085}\TypeLib = {3DB63102-0FED-11D0-9A44-0080C70FB085}  ; TypeLibrary registrationHKEY_CLASSES_ROOT\TypeLib\{3DB63102-0FED-11D0-9A44-0080C70FB085}HKEY_CLASSES_ROOT\TypeLib\{3DB63102-0FED-11D0-9A44-0080C70FB085}\1.0 = first Type LibraryHKEY_CLASSES_ROOT\TypeLib\{3DB63102-0FED-11D0-9A44-0080C70FB085}\1.0\0\win32 = first.tlb

OLE Server Instancing

If from the Project menu you choose Project Info for the FIRST project, you'll see that there are three panes of information. The third is called Servers, from which you can view or change the information relating to each OLE Public in your project. Note that this information will only appear after you've built your project into an EXE or DLL, because only then is the OLEPUBLIC keyword meaningful.

The Instancing drop-down allows you to specify how an out-of-process server will behave. If this is set at "Single Use" and builds an EXE, then each OLE Automation client that instantiates this server will get its own private instance of the server. For example, if you type OX = CreateObject("first.myform ") and then OY = CreateObject("first.myform"), you'll actually get two running processes. (Note the SET EXCLUSIVE OFF line in the LOAD event; this is important!) You can use tools like Task Manager to see two first.exes running. This will also be the case if there were two different clients, say, Visual Basic and Visual FoxPro.

If you change the instancing on "Myform" to "Multiple Use" (which means this class can be used multiple times, or can have multiple "Masters"), then instantiate both OX and OY as above, TaskManager shows only a single Server instance serving multiple "Masters." You'll also note that moving the record pointer in one instance will change it in the other. You can change the DataSession property to 2—PRIVATE to avoid this.

Page 24: Webcrawl a Blog to Retrieve All Entries Locally

The third option on Instancing is "Not Creatable." Why would you want this? Suppose you have a Class library with at least one OLE Public in it for one project. Now suppose you want to use some of the classes in that class library for another project, but you didn't want to use that OLE Public class. Using this instancing option means that the OLE Public will be ignored to solve this problem.

Automating a FoxPro 2.x Application

With just a few lines of code, you can take a FoxPro 2.x program and make it an OLE server. For example, the Laser sample that ships with FoxPro 2.6 is a laser disk library manager program. The main program is Laser.SPR. To turn this into an OLE object callable from Visual Basic or Excel, create a new project and add a new main program:

ox = create("laser")define class laser as custom olepublic    proc init        cd d:\fpw26\sample\laser    && change dir to the right place        set path to data        this.application.visible = .t.  && make us visible.    proc doit        do laser.sprenddefine

Add laser.spr and rebuild the project. You'll need to manually add a couple of Laser files. Now, you can try OX = Createobject("laser.laser"), and then OX.Doit. The laser application is now running as an OLE Automation server! If you modify the LASER.SPR program so that it doesn't close the LASER table when the READ (remember this command?) is finished, then you can query what laser disc title was chosen: ?ox.application.eval("title")

Your First Dynamic Web Page

Your FIRST.PJX project contained a simple OLE server that just put up a message box. This is not very useful, especially since most OLE servers should be able to run on an unattended server machine. Let's modify this sample to generate HTML strings. Add to your FIRST.PRG the class definition for dbpub:

DEFINE CLASS dbpub AS custom OLEPUBLIC    cDataPath = "c:\vfp\samples\data"        && change this to point to the dir where your TESTDATA.DBC is    cDir = ""    cSrvName = "first"    PROCEDURE INIT        CLOSE DATA ALL        SET TALK OFF        SET SAFETY OFF        SET EXCLUSIVE OFF        SET DELETED ON        DECLARE INTEGER GetPrivateProfileString in win32api String,String,String,;              String @, integer,string        if _vfp.startmode>1    &&  automation server            LOCAL buf,nlen            buf=space(400)            DECLARE INTEGER GetModuleFileName in win32api Integer,String @,Integer            * As an EXE or DLL, we need to find the home directory, not the curdir() and not            * the dir of the runtime. The classlibrary property             * shows this for PRGs, but not VCXs            IF _vfp.startmode = 3 && inproc dll                DECLARE INTEGER GetModuleHandle in win32api String                nlen=Getmodulefilename(GetModuleHandle(this.csrvname+".dll"),@buf,len(buf))

Page 25: Webcrawl a Blog to Retrieve All Entries Locally

            ELSE                nlen=Getmodulefilename(0,@buf,len(buf))            ENDIF            buf = LEFTC(buf,nlen)            this.cdir = LEFTC(buf,RATC('\',buf)-1)            this.csrvname = SUBSTRC(buf,RATC('\',buf)+1)            this.csrvname = LEFTC(this.csrvname,AT_C(".",this.csrvname)-1)        endif         IF !EMPTY(this.cDir)            SET DEFAULT TO (this.cDir)        ENDIF          SET PATH TO (this.cDatapath)    PROCEDURE dbpub(parms,inifile,relflag)        LOCAL rv        rv = "HTTP/1.0 200 OK"+CHR(13)+CHR(10)        rv = m.rv + "Content-Type: text/html"+CHR(13)+CHR(10)+CHR(13)+CHR(10)        rv = m.rv + "<HTML>"        rv = m.rv + "Parm1 =" + m.parms + "<p>"        rv = m.rv + "Parm2 =" + m.inifile        RETURN m.rvENDDEFINE

Note that the INIT method here is almost identical to the LOAD event for the form above. I've just removed the USE EMPLOYEE line. The DBPUB method takes three parameters and returns an HTML string. Test it out with ox=CreateObject('first.dbpub') then ?ox.dbpub("parm1","parm2",1234).

Now we need to find an Internet Web server. You can use Windows NT® 4.0 Server, which comes with Microsoft Internet Information Server (IIS), or NT3.51, Service Pack 4 with IIS. Windows 95 and Windows NT 4.0 Workstation will also work with Personal Web server. Other ISAPI compatible servers have been rumored to work. Take the file VFP\SAMPLES\SERVERSFOXISAPI\FOXISAPI.DLL (note that the latest copy of this can be found on the Microsoft Visual FoxPro Web site) and place it in the SCRIPTS directory of your IIS.

Suppose the Web site is named "myweb." Start up a Web browser, and type in the URL: http://myweb/scripts/foxisapi.dll. You should get a Foxisapi error page, indicating that the DLL is working correctly. Try adding arguments, such as http://myweb/scripts/foxisapi.dll/arg1.arg2.method?test. The first and second arguments are interpreted as the ProgID of an OLE Automation server. The third parameter is interpreted as the method name, and whatever is after the "?" is interpreted as a parameter to pass.

Before we run your FIRST server from the web, we need to tend to some security issues if you're running on Windows NT 4.0. IIS runs as a service on NT, which means it can run with no user logged in. It also has very limited rights to various parts of the operating system. Services normally do not have a desktop, so a MessageBox from a service won't even show up on the screen and will just hang the service.

Windows NT 3.51 will allow any service rights to read the registry and launch an OLE server. This means you have little control over somebody attaching to your machine and using it to run tasks. If you run the IIS service manager, and look at the WWW service properties, you'll notice that the anonymous user logs in as IUSR_MYWEB. From the Visual FoxPro command window, type !/n DCOMCNFG. This is an Windows NT 4.0 utility that you must run after registering an OLE server. Add "IUSR_MYWEB" to the three button dialogs on the DCOMCNFG Default Security page. Look for "myfirst" in the list of applications on the Applications Page. Choose Properties> Identity, and run as the Interactive User. Your particular machine/network setup might vary from these procedures.

Because Visual FoxPro registers the server each time after a build, you may have to run DCOMCNFG after each build. You can just run it and exit without changing anything. An alternative is to connect to the server machine from another machine and just copy the new server EXE or DLL over the old one (assuming it's already registered on the server machine).

Now that we've got security, lets hit our Web site with "myweb/scripts/foxisapi.dll/myfirst.dbpub.dbpub?test" The returned HTML string shows the parameters. The first parameter is just whatever is after the "?". The second parameter is the name of a .INI file that contains information about the Web hit. You can see this information get parsed out if you do the following:

Page 26: Webcrawl a Blog to Retrieve All Entries Locally

MODIFY CLASS isform OF HOME()+"samples\servers\foxisapi\isapi" METHOD genhtml

Here's a sample .INI file:

The INI file looks like this:[FOXISAPI]Request Method=GETQuery String=Logical Path=/foxis.employee.startupPhysical Path=d:\inetsrv\wwwroot\foxis.employee.startupFoxISAPI Version=FoxISAPI v1.0Request Protocol=HTTP/1.0Referer=/scripts/foxisapi.dllServer Software=Microsoft-Internet-Information-Server/1.0Server Name=127.0.0.1Server Port=80Remote Host=127.0.0.1Remote Address=127.0.0.1[All_HTTP]HTTP_ACCEPT=*/*, q=0.300,audio/x-aiff,audio/basic,image/jpeg,image/gif,text/plain,text/htmlHTTP_USER_AGENT=Mozilla/1.22 (compatible; MSIE 1.5; Windows NT)HTTP_CONNECTION=Keep-AliveHTTP_EXTENSION=Security/Digest[Accept]*/*=Yesq=0.300=Yesaudio/x-aiff=Yesaudio/basic=Yesimage/jpeg=Yesimage/gif=Yestext/plain=Yestext/html=Yes[SYSTEM]GMT Offset=-28800

The third parameter is just a number passed in by reference. It's actually meaningless to the server. Each time a Web hit occurs, foxisapi.dll will instantiate the ProgID, invoke the method passing the parms, return the generated HTML to the Web site, and then release the OLE server. It's pretty inefficient to start and stop the OLE server each time. However, if the server changes the value to 0, then the server won't be released and will be already running and ready for the next Web hit. To release the server, it need only not alter the value.

To release all Visual FoxPro servers on a single server, you can use the URL: "/scripts/foxisapi.dll/reset". This will call all cached existing instances of Visual FoxPro servers and tell them to release.

Database Publishing on the Web

Now that we know how to have Visual FoxPro OLE servers generate HTML, let's get a little fancier and publish a .dbf on the Web. Change the DBPUB method to the code below:

    PROCEDURE dbpub(parms,inifile,relflag)        LOCAL rv,m.ii,mt        rv = "HTTP/1.0 200 OK"+CHR(13)+CHR(10)        rv = m.rv + "Content-Type: text/html"+CHR(13)+CHR(10)+CHR(13)+CHR(10)        rv = m.rv + "<HTML>" + '<table border = "10">' + chr(13)        USE (m.parms) SHARED    && Open the table        for ii = 1 to FCOUNT()            IF type("EVAL(FIELD(m.ii))") = 'G'                loop    && don't proc general fields

Page 27: Webcrawl a Blog to Retrieve All Entries Locally

            ENDIF            rv = rv + "<th>" + PROPER(field(m.ii)) + "</th>" + chr(13)        endfor        rv = rv + "</tr>" + chr(13)        SCAN            rv = rv + "<tr>"            for ii = 1 TO FCOUNT()                IF type("EVAL(FIELD(m.ii))") = 'G'                    loop    && don't proc general fields                ENDIF                mt = eval(field(m.ii))                do case                case type("mt") = 'C'                    rv = rv + "<td>" + mt + "</td>"                case type("mt") = 'T'                    rv = rv + "<td>" + TTOC(mt) + "</td>"                case type("mt") $ 'NY'                    rv = rv + "<td align=right>" + str(mt,8) + "</td>"                case type("mt") = 'D'                    rv = rv + "<td>" + DTOC(mt) + "</td>"                endcase                rv = rv + chr(13)            endfor            rv = rv + "</tr>"        ENDSCAN        rv = rv +"</table>"        return rv

The first parameter specifies the name of the table. All this does is loop through all the columns and rows in a DBF and returns an HTML Table string. Try it with a few URLs: "HTTP://myweb/scripts/foxisapi.dll/first.dbpub.dbpub?customer", "HTTP://myweb/scripts/foxisapi.dll/first.dbpub.dbpub?employee".

By just putting the name of a table in a URL, it is published on the Web. Pretty powerful stuff!

Deploying Your Visual FoxPro Applications Over the Internet

In your Visual FoxPro SAMPLES\SERVERS directory is a directory called FOXISAPI. This is a sample form class that just allows the user to edit/view the EMPLOYEE data in TESTDATA.DBC. However, the same form can be deployed in four different ways: as a normal Visual FoxPro form, as a Visual FoxPro run time, as an OLE Automation server from an OLE Automation client, and over the Internet using any Web browser!

This sample requires only that end users have a machine capable of running any Internet browser, which means it will allow you to deploy Visual FoxPro applications on 286s, Macs, Unix boxes, and maybe even personal digital assistants! Any changes made to the application only need to be done once. The changes will be automatically propagated to the other deployment platforms.

The readme.txt file in that directory explains how to install and configure the sample. For the latest version of this sample, go to the Visual FoxPro home page at http://www.microsoft.com/vfoxpro/. Also be sure to read the comments in foxisapi.cpp and isapi.vcx for tips.

The FOXISAPI sample takes advantage of the third parameter to do smart instancing of the server. The CMD method allows the user to execute DOS commands or evaluate FoxPro expressions on the server. If RESET is passed as a DOS command, the server will not change the third parameter to 0, and thus the server instance gets released.

Most of the work in the FOXISAPI sample is done by the GENHTML method. The result is a two column HTML table with labels for the field names and text box controls for the field values. It does an AMEMBERS( ) function call to determine dynamically what objects are on the form and to place the objects into an array. It then does a two-dimensional bubble sort to sort the objects into x,y order. HTML is stream based, whereas Visual FoxPro is pixel based.

Page 28: Webcrawl a Blog to Retrieve All Entries Locally

The method then loops through each form member in the array elements and tries to generate HTML for them. If the object has its own GENHTML method, then it is invoked to return an HTML string. The class of the object is examined to determine what kind of object it is, and the appropriate HTML is generated. If there's a text box, then an associated label is searched for. Both of these get entered into the HTML table for the form.

After the form is generated, GENHTML then looks at the .INI file and appends the data to a table and the returned HTML.

Web servers can be hit multiple times by multiple clients. If client A clicks NEXT and client B clicks NEXT, it's important that the server knows which record was current for each client. This is handled with a cookie, which is just a unique ID assigned to the client. Subsequently served Web pages to that client include the same cookie hardcoded in its HTML.

Error Handling is extremely important from an OLE server, especially one that serves up Web pages. The server is typically unattended, and often doesn't even have a desktop to which error dialogs can be displayed. If there's an error in the FOXISAPI sample, the error method generates an Error HTML page and the GENHTML method returns that error page.

The LOG method in the FOXISAPI sample is useful: it just adds a line to a text file. The file can be examined to see what the server is doing. Use this liberally when debugging your applications. Microsoft Visual Studio™, which comes with Visual C++, has an option to automatically reload externally modified files, so you can just watch the log as the file changes.

Some parameters are passed via URLs, and thus some characters are not legal. For example, the chars "<>()" are not legal. They are converted by the Web browser to their hexadecimal equivalents prepended by a "%". The FIXURL method reverses the conversion.

Who Hit You?

When your Web server is hit from a Web site, how do you know who hit you? There are three new functions added to Foxtools that will help determine this. It also depends on whether you're on an intranet or the Internet. These functions call the Win Sockets API to resolve a remote IP Address to a host name.

I've added three functions to Foxtools that will resolve IP addresses.

    {"_WSOCKSTARTUP",    (FPFI) WSockStartup, 7,  "I,R,R,R,R,R,R"},    {"_WSOCKCLEANUP",    (FPFI) WSockCleanup, 0,  ""},    {"_WSOCKGETHOSTBYADDR",    (FPFI) WSockGetHostByAddr, 2,  "C,R"},

Call _WSOCKSTARTUP and _WSOCKCLEANUP to initialize/uninitialize the Winsock library. The _WSOCKGETHOSTBYADDR function takes an IP address string like "123.123.123.123" and resolves it into the name of the machine.

// store 0 to buff1,buff2,buff3,buff4,buff5,buff6//?_wsockstartup(256 * 1 + 1,@buff1,@buff2,@buff3,@buff4,@buff5,@buff6)// integer version, wVersion,wHighVersion,szDescription,szSystemStatus,iMaxSockets,iMaxUdpDg  //?    _wsockgethostbyaddr("123.123.123.123",@buff1)      &&returns 1 on success, 0 on failure//        ?buff1                && returns the host name// Note: if the target machine is not available, this call can take a long time.

Active Server Pages

Using Internet Information Server 3.0 with Active Server Framework (with the code name Denali) you can have your Web server send Active Server Pages (ASP). These ASP files are HTML files with special tags enclosed in the delimiters "<%" and "%>". These tags delimit both client-side and server-side scripts. Those that are server side will be executed on the server and will not show up on the client side (when the client Internet browser does a View>Source, for example).

Page 29: Webcrawl a Blog to Retrieve All Entries Locally

The server-side scripting can be in any scripting language, but defaults to VBScript. You can create an ASP file that has a few lines of script that will return HTML for your ISAPI form:

<% set ox = server.CreateObject("foxis.employee")%><% = ox.Startup()%>

Here, a server script variable called ox is assigned an object reference to the Visual FoxPro automation server "foxis.employee". The second line invokes the startup method on the object, and returns the results as HTML.

In this case, the OLE server instancing model is handled by Denali. Because ox is a script variable, its lifetime is the lifetime of the script. That means the entire Visual FoxPro server instance is instantiated and released for each Web hit. Denali provides variables scoped to a session to allow the OLE server to have a longer lifetime:

<% set session("ox") = server.CreateObject("foxis.employee")%><% = session("ox").Startup()%>

Instead of using a script variable, we're using a session variable called ox.

Using this method of starting the OLE server, the server instance will exist until the session.timeout (default 20 minutes) expires.

The ISFORM class of ISAPI has a property called fASP which is either True or False, indicating whether or not the class was instantiated from an ASP page or from an HTML page (using the HREF /scripts/foxisapi.dll/foxis.employee.startup). If this flag is true, then the server can return generated HTML without an HTML Content header (there already is one in the ASP page. Also, an ASP page generates a cookie that can be used as the unique identifier for the session, and this is parsed out in the STARTUP method.

<% set session("ox") = server.CreateObject("foxis.employee")%><% = session("ox").Startup("ASP Cookie=" + Request.ServerVariable("http_cookie"),"",0)%>

Other Uses of OLE Servers

OLE was designed so that an out-of-process automation server does not have to live on the same machine as the client. It can be run on any other machine on the network. This remote automation capability means that OLE server applications don't care whether they're to be deployed on a local machine or a remote one. Also, because a single server can serve multiple clients, new classes of client/server application architectures are possible.

OLE servers allow application developers to deploy their applications as OLE clients or OLE servers, or both. That is, an application can have two or more components: one that acts as an OLE client, and one that acts as an OLE server. For example, a customer form might be a simple OLE client application running on dozens of machines, but it might talk to a single OLE server application that lives on a server machine somewhere that might enforce business rules, such as no new orders from a customer who has outstanding debts. The business rule enforcer might then talk to the actual data itself, which might be on Microsoft SQL Server or Visual FoxPro tables on yet another machine. If the customer form were to change, then only the front end needs to be updated. If the business rules change, then only the single business rule enforcer needs to be replaced. This "three-tier" client/server architecture also means that data processing loads get distributed to specialized applications/machines designed and optimized to do the job.

Another useful architecture is to have a client application spin off computationally- or resource-intensive tasks to other machines, so the client machine is free for other tasks such as printing, database updating, report generation, end-of-month processing, and so on.

 

Published Thursday, September 13, 2007 5:43 PM by Calvin_Hsia 

Filed under: Visual FoxPro, History, Web

Comment Notification

Page 30: Webcrawl a Blog to Retrieve All Entries Locally

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Here’s how you can use Visual Studio to create a .Net User Control that will act as an ActiveX control that can be used in Excel (or other Office application), VB6 or Foxpro. This will bring the power of the entire.Net Framework to your program. This sample uses a Web Service to look up zip codes. I browsed some web services available at www.xmethods.net and found one that returns information on a zip code. (You can also try a MapPoint Web Service: http://msdn.microsoft.com/mappoint/default.aspx?pull=/library/en-us/dnmapnet30/html/MPWS_AND.asp) Start Visual Studio 2005, choose File->New->Project->VB Windows Class Library. Call it VBZipWebService. From the Solution Explorer, delete Class1.vb, then right click on the VBZipWebService project (not the VBZipWebService solution), choose Add New Item, choose User Control, call it VBZipWebService.vbFrom Project->Properties->Application, make sure the Root Namespace is VBZipWebService and not ClassLibrary1. (If you don’t the ProgId for the control will be ClassLibrary1.VBZipWebService). Now the ProgId will be VBZipWebService.VBZipWebServiceYou just created a control with a design surface. Add a Button and a DataGridView.  Right click the project again, choose Add Web Reference, then paste in this URL: http://www.jasongaylord.com/webservices/zipcodes.asmx?wsdl as the WSDL (Web Service Description Language), Click Go to see the Web methods, then Add Reference. Note that the “Web Reference Name” is “com.jasongaylord.www” Paste in this code: Public Class VBZipWebService    Dim _Zipcode As String = "98052"    Public Property ZipCode() As String        Get            Return _Zipcode        End Get        Set(ByVal Value As String)            _Zipcode = Value        End Set    End Property     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click        Dim ozip As New com.jasongaylord.www.ZipCodes        Dim ds As Data.DataSet = ozip.ZipCodeToDetails(Me._Zipcode)        Me.DataGridView1.AutoGenerateColumns = True        Me.DataGridView1.DataSource = ds.Tables(0)        Me.DataGridView1.AutoResizeColumns()    End SubEnd Class 

Page 31: Webcrawl a Blog to Retrieve All Entries Locally

Hit the F5 button to test your UserControl in the UserControl TestContainer. Click the button and wait a few seconds for the se Now that you have it working the way you want, let’s make this UserControl act like a normal ActiveX control that can be used from Foxpro, Excel, VB6.  Choose Project->Properties->Compile tab. Make sure “Register For COM interop” checkbox is checked. Choose Project->Add New Item->Module. Call it Comregistration.vb, and paste this code into it:  Imports Microsoft.Win32Imports System.Windows.Forms Friend Module ComRegistration    Public Sub RegisterControl(ByVal t As Type)        Try            GuardNullType(t, "t")            GuardTypeIsControl(t)            ' CLSID            Dim key As String = "CLSID\" & t.GUID.ToString("B")            Using subkey As RegistryKey = Registry.ClassesRoot.OpenSubKey(key, True)                ' Control                Using controlKey As RegistryKey = subkey.CreateSubKey("Control")                End Using                ' Misc                Using miscKey As RegistryKey = subkey.CreateSubKey("MiscStatus")                    miscKey.SetValue("", "131457", RegistryValueKind.String)                End Using                ' TypeLib                Using typeLibKey As RegistryKey = subkey.CreateSubKey("TypeLib")                    Dim libId As Guid = System.Runtime.InteropServices.Marshal.GetTypeLibGuidForAssembly(t.Assembly)                    typeLibKey.SetValue("", libId.ToString("B"), RegistryValueKind.String)                End Using                ' Version                Using versionKey As RegistryKey = subkey.CreateSubKey("Version")                    Dim major, minor As Integer                    System.Runtime.InteropServices.Marshal.GetTypeLibVersionForAssembly(t.Assembly, major, minor)                    versionKey.SetValue("", String.Format("{0}.{1}", major, minor))                End Using            End Using        Catch ex As Exception            HandleException("ComRegisterFunction failed.", t, ex)        End Try    End Sub     Public Sub UnregisterControl(ByVal t As Type)        Try

Page 32: Webcrawl a Blog to Retrieve All Entries Locally

            GuardNullType(t, "t")            GuardTypeIsControl(t)            ' CLSID            Dim key As String = "CLSID\" & t.GUID.ToString("B")            Registry.ClassesRoot.DeleteSubKeyTree(key)        Catch ex As Exception            HandleException("ComUnregisterFunction failed.", t, ex)        End Try    End Sub    Private Sub GuardNullType(ByVal t As Type, ByVal param As String)        If t Is Nothing Then            Throw New ArgumentException("The CLR type must be specified.", param)        End If    End Sub    Private Sub GuardTypeIsControl(ByVal t As Type)        If Not GetType(Control).IsAssignableFrom(t) Then            Throw New ArgumentException("Type argument must be a Windows Forms control.")        End If    End Sub     Private Sub HandleException(ByVal message As String, ByVal t As Type, ByVal ex As Exception)        Try            If t IsNot Nothing Then                message &= vbCrLf & String.Format("CLR class '{0}'", t.FullName)            End If            Throw New Exception(message, ex) ' replace with custom exception type        Catch ex2 As Exception            My.Application.Log.WriteException(ex2)        End Try    End SubEnd Module  As you can see, this code has a couple methods to add a few registry keys on Registering and Unregistering. Now we need to have this code called when the assembly is registered or unregistered. The ComRegisterFunctionAttribute class helps here. (ComRegistration.vb is reusable as is with all your controls) When you build, the assembly is automatically registered and these methods are called.  We add 2 methods to use these attributes, an Imports statement, and a Public Event to show how we can get events from the control.  We also add an attribute to your Class line<Microsoft.VisualBasic.ComClass()> Public Class VBZipWebService (Alternatively, you can move your cursor to that line and double click the property sheet Com Class property to make it True. Also, make sure the Project->Properties->Compile->Register for COM Interop checkbox is checked.) Now your code looks like this: Imports System.Runtime.InteropServices<Microsoft.VisualBasic.ComClass()> Public Class VBZipWebService    Public Event GotData(ByVal City As String)    Dim _Zipcode As String = "98052"

Page 33: Webcrawl a Blog to Retrieve All Entries Locally

    Public Property ZipCode() As String        Get            Return _Zipcode        End Get        Set(ByVal Value As String)            _Zipcode = Value        End Set    End Property     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click        Dim ozip As New com.jasongaylord.www.ZipCodes        Dim ds As Data.DataSet = ozip.ZipCodeToDetails(Me._Zipcode)        Me.DataGridView1.AutoGenerateColumns = True        Me.DataGridView1.DataSource = ds.Tables(0)        Me.DataGridView1.AutoResizeColumns()        RaiseEvent GotData(ds.Tables(0).Rows(0).Item("city"))    End Sub    <ComRegisterFunction()> _    Public Shared Sub Register(ByVal t As Type)        ComRegistration.RegisterControl(t)    End Sub     <ComUnregisterFunction()> _    Public Shared Sub Unregister(ByVal t As Type)        ComRegistration.UnregisterControl(t)    End Sub End Class Hit F5 to build and see that it works as before. From Excel Hit Alt-F11 (Tools->Macros->Visual Basic Editor), then Insert->User Form.  From VB6 and Excel, right click on the toolbar and choose “Additional Controls”(Excel) or “Components…”(VB6). Drag the control onto the form and hit F5 You can also handle the event by adding this code in Excel:

Private Sub VBZipWebService1_GotData(ByVal City As String)MsgBox ("got data " & City)

End Sub 

From Foxpro, run this code: PUBLIC oxox=CREATEOBJECT("myform")ox.show  DEFINE CLASS myform AS form          left=300          allowoutput=.f.          width=400          height=300          PROCEDURE Init                   this.AddObject("ocZip","myoc")                   this.ocZip.width=thisform.Width                   this.ocZip.height=thisform.Height                   this.ocZip.visible=1ENDDEFINE

Page 34: Webcrawl a Blog to Retrieve All Entries Locally

  DEFINE CLASS myoc AS olecontrol          oleclass="VBZipWebService.VBZipWebService"          PROCEDURE init                   this.object.zipcode="96825"          PROCEDURE Gotdata(p1,p2)                   ?PROGRAM(),p1 ENDDEFINE Alternatively, Foxpro Tools->Options->Controls->ActiveX Controls will allow you to register any ActiveX control so it shows up on the Form Designer toolbar.  Additional notes: The Event can pass a .Net object as a parameter: Just add an attribute: 

Public Event DocObjectUnknown(<MarshalAs(UnmanagedType.IUnknown)> ByVal sender As Object) That way the client gets an object reference to the .Net object (which may be different from the user control). To rebuild the control, you have to close the client process (Fox, Excel, VB6) to unload the CLR. Click the Solution Explorer->Show All Files button and expand the web reference and look at some of the generated files. (Thanks to PaulYuk and AbelV for their help) See also: A Visual Basic COM object is simple to create, call and debug from ExcelUse Regular Expressions to get hyperlinks in blogs I just finished composing this blog entry and I found Craig Boyd’s sample.Published Friday, July 14, 2006 9:47 AM by Calvin_Hsia Filed under: Visual FoxPro, VB, Windows API, Web

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# Use a different kind of grid in your applications

Tuesday, July 18, 2006 9:34 PM by Calvin Hsia's WebLog My prior post (Create a .Net UserControl that calls a web service that acts as an ActiveX control to...

Page 35: Webcrawl a Blog to Retrieve All Entries Locally

# Use a different kind of grid in your applications &raquo; Wagalulu - Microsoft &raquo; &raquo; Use a different kind of grid in your applications

Tuesday, July 18, 2006 9:42 PM by Use a different kind of grid in your applications » Wagalulu - Microsoft » » Use a different kind of grid in your applications PingBack from http://microsoft.wagalulu.com/2006/07/18/use-a-different-kind-of-grid-in-your-applications/

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Tuesday, July 18, 2006 11:25 PM by Kevin Dente Hmm...last time I heard, only IE and MFC7 are supported ActiveX control containers for .NET controls.

http://support.microsoft.com/kb/311334/en-us

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Wednesday, July 19, 2006 6:46 PM by VBTeam True, VB6 is not a supported ActiveX control container for .NET User Controls today. However, Calvin and I are working together to see if it is possible longer term to add this as a MS Supported container.  Meanwhile, guidance like this helps you understand the current limitations and navigate around them (unsupported).

Paul Yuknewicz (Paulyuk) VB Program Manager

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Tuesday, August 01, 2006 5:19 AM by Joris Schoot I have tried your example.. It works fine in Excel. But in VB6 the event doesn't raise. Any clue?

Regards, Joris

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Thursday, August 03, 2006 3:06 AM by Calvin_Hsia Yes Joris, a .Net control will fire events fine in Excel and Foxpro, but not in VB6. This is a known issue, currently being investigated.

# Host the CLR and Generate IL to call a MessageBox

Monday, August 07, 2006 6:07 PM by Calvin Hsia's WebLog

Page 36: Webcrawl a Blog to Retrieve All Entries Locally

Here’s some C++ code to host the CLR. It’s an alternative to using COM Interop (see A Visual Basic COM...

# Create an ActiveX control using ATL that you can use from Fox, Excel, VB6, VB.Net

Monday, August 28, 2006 8:08 PM by Calvin Hsia's WebLog Creating an ActiveX control is a good exercise in understanding how one works. It also helps to have...

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Friday, October 20, 2006 2:14 PM by Jeff

can this same method be used for embedding the activex in a browser?

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Thursday, November 23, 2006 8:40 AM by Avy

I have tried your example.. It works fine in VB6 but on this usercontrol in VB6 I don't have any  events (like Click, MouseDown,MouseUp)  only DragDrop,DragOver, LostFocus,GotFocus and Validate events.

Do you have any clue about this?

Thx

Avy

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Thursday, November 23, 2006 12:46 PM by Rubio

How can I export a  c#.Net object as an ocx/activex/ecc.. to use in old style unmanaged application ( the old c++ builder5 or other c++ )I would like to use a procedure that doesen't use VB...

do you have some idea?

Thx

R.

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Tuesday, January 09, 2007 11:43 AM by Avy

Page 37: Webcrawl a Blog to Retrieve All Entries Locally

Hi again.

I have another usercontrol problem.

I make a usercontrol(that inherits usercontrol class) in VB.net 2005 that contains a toolstrip control and on that toolstrip I add in runtime  9 buttons.

I make this usercontrol as tlb and I try to add in VB 6 as components and it give me an error: "Name conflicts with existing module, project, or object library"

Do you have any solution for that?

When I develop a usercontrol that inherits button class and not usercontrol class it is working.I think this is a problem but I am not so sure.

Could anyone help me with that, please?

Thks

Avy

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Monday, February 19, 2007 6:58 AM by David

This is a great example! thanks.

It's all working nicely on my development machine.

Does anyone have any experience with deploying a VB6 app that uses a .net control?  

# How to Debug VB6 Activex Usercontrol from VB.Net

Thursday, March 01, 2007 1:13 AM by Manoharan

How to Debug VB6 Activex Usercontrol from VB.Net

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Thursday, May 03, 2007 10:41 AM by med111

hello,

In my case, this works fine in tstcon.exe but not in excel, (maybe I missed some thing). I can add this activex in vba editor "additional controls" but when I add to UserForm1, there was an error: the system cannot find the file specified. Moreover the control icon in toolBox has "Unknown" as tooltipBox ,Hope I have a reply!!

Page 38: Webcrawl a Blog to Retrieve All Entries Locally

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Tuesday, October 09, 2007 2:36 PM by Injuninus

When I Rebuild the .NET Assembly, the reference in VB6 breaks. I get an exception "Object library invalid or contains references to object definitions that could not be found".

# Use WPF and inline XAML in your Fox, Excel or VB6 applications

Wednesday, December 12, 2007 1:59 PM by Calvin Hsia's WebLog

My prior post showed how to create XAML WPF and put it on your Winform App. We can go one step further:

# Use WPF and inline XAML in your Fox, Excel or VB6 applications

Wednesday, December 12, 2007 2:05 PM by Noticias externas

My prior post showed how to create XAML WPF and put it on your Winform App. We can go one step further

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Tuesday, April 08, 2008 11:37 PM by prajakta

Hi All,

I have created user control in vb.net but it does not handle events like mouseup,mousedown.

Please reply as soon as possible.

Thanx

# re: Create a .Net UserControl that calls a web service that acts as an ActiveX control to use in Excel, VB6, Foxpro

Thursday, May 22, 2008 9:13 PM by Beto

Hi All

can you help me to invoke this control from html

sample

      <OBJECT id="myControl1" name="myControl1" classid="VBZipWebService.dll#VBZipWebService.VBZipWebService" width=288 height=72>

Page 39: Webcrawl a Blog to Retrieve All Entries Locally

      </OBJECT>

this don't work

Creating a VFP application as a service:

Sometimes it’s useful to make your application run as a service. A service can run without any user logged in, can automatically start upon reboot, and can survive user logoffs. It can also run as a different user with different access rights. Normally, Creating a service is fairly complex, but creating a VFP application as a service is fairly straightforward, thanks to a pair of tools called INSTSRV.EXE and SRVANY.EXE that come with the Windows Resource Kit. INSTSRV.EXE takes 2 parameters: the name of the new service to install, and the full path to the SRVANY.EXE file. For example, to create a service called “VFPSrv”, I ran this on my Windows XP machine:C:\Program Files\Windows Resource Kits\Tools>instsrv VFPSrv "d:\Program Files\Windows Resource Kits\Tools\srvany.exe"  SRVANY provides all the main work to pretend to be a service, and it will look in the registry to find what EXE file to run to implement the service. The EXE name, the starting directory, and any optional parameters are found in the registry. Copy/paste the following to a file named VFPSRV.REG (change the paths as necessary: note the double back-slashes) and dbl-click the .REG file to add these entries to the registery. Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\VFPSrv\Parameters]"AppDirectory"="d:\\fox90\\test""Application"="d:\\fox90\\test\\vfpsrv.exe""AppParameters"="myparm1 myparm2"  You can use the VFP command window to control the service:!net start vfpsrv!net stop vfpsrv or the DOS command window: net start vfpsrv     The sample service code below executes a file called “vfpsrvrtn.prg” when the timer fires. However, this code is not built into the EXE. This means you can change the service’s behavior dynamically, without having to stop/rebuild/start the service. The code executes the timer event every 5 seconds, aligned to the 5 second real time clock (so that it occurs at exactly :00, :05, :10, … seconds past the hour).If you run the prg below, it will run as a normal VFP PRG, but it will also build VFPSRV.EXE.  To start it as a service, use the control panel->Administrative Tools->Services or “net start vfpsrv” 

Page 40: Webcrawl a Blog to Retrieve All Entries Locally

Open the log file in an automatically refreshing editor, such as Visual Studio, from another machine and then log off the main machine. You can still hear the beeps and see the log file changing when no user is logged in. (using older versions of SRVANY or on older versions of Windows, you may need to use SYS(2340) ) *Save this content to a file called VFPSRV.PRGLPARAMETERS parm1,parm2CLEAR* http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tools/tools/service_control_utility.asp* http://www.microsoft.com/msj/1097/WINNT.aspx *To get SrvAny and SrvInst: go to http://www.microsoft.com/downloads, enter Windows 2003 Resource Kit Tools in the Keywords field, and click Go. *Then, click the Windows Server 2003 Resource Kit Tools Download button at the Windows Server 2003 Resource Kit Tools *Web page to download rktools.exe-which contains the most recent versions of Instsrv and Srvany-and run the executable to install the tools on your system.  #define TIMERINT  5     && # of seconds for timer interval. Also syncs to TIMERINT seconds for time of day PUBLIC oServiceoService=NEWOBJECT("vfpsrv")oService.logstr("Starting Service: got some params: "+TRANSFORM(parm1)+" "+TRANSFORM(parm2))IF _vfp.StartMode>0     && if we’re running as an EXE      READ events       && message loopENDIF  *Create VFPSRVRTN.PRG that just beepsSET TEXTMERGE TO vfpsrvrtn.prg      \LPARAMETERS oTimer,oService      \oService.logstr("")      \Messagebeep(0)SET TEXTMERGE TO  *RETURN  BUILD PROJECT vfpsrv from vfpsrv MODIFY PROJECT vfpsrv nowaitSTRTOFILE("screen=off"+CHR(13)+CHR(10),"config.fpw")  && to hide the VFP runtime desktop_vfp.ActiveProject.Files.Add("config.fpw")_vfp.ActiveProject.CloseBUILD EXE vfpsrv FROM vfpsrv ERASE config.fpw DEFINE CLASS vfpsrv AS form      PROCEDURE Init            this.logstr(CURDIR())            DECLARE integer MessageBeep IN WIN32API integer            this.AddObject("mytimer","mytimer")            WITH this.mytimer                  .enabled=.t.                  .interval=1000    && start it in a sec            ENDWITH       PROCEDURE Destroy

Page 41: Webcrawl a Blog to Retrieve All Entries Locally

            ?PROGRAM()            MessageBeep(32)      PROCEDURE logstr(str as String)            str=TRANSFORM(DATETIME())+" "+str+CHR(13)+CHR(10)            ??str            STRTOFILE(str,"c:\vfpsrv.log",.t.)ENDDEFINEDEFINE CLASS mytimer AS timer      PROCEDURE timer            DO ("vfpsrvrtn" ) with this,oService            CLEAR PROGRAM            dtNow=DATETIME()  && read datetime() and seconds() close to the same instant            nsec=SECONDS()            nsec=CEILING((nsec+.5)/TIMERINT)*TIMERINT && target is # of seconds since midnight + a little for calculation time            dtTarget=DTOT(DATE())+nsec            this.interval=(dtTarget-dtNow) * 1000 ENDDEFINE  45776Published Monday, December 13, 2004 1:49 PM by Calvin_Hsia Filed under: Visual FoxPro, Windows API