Thursday, February 02, 2012, 3:52:27 PM
Gravatar.com is a great and very popular Web service based way to associate an image with user accounts that are based on email addresses. It's easy to use and can provide a nice touch of personalization with very little effort to just about any Web application that uses email addresses for users. This post shows how you can use Gravatar from within your FoxPro based applications.
I recently restyled my West Wind Message Board support site and one of things that I think makes the messages look a little more interesting and personal is having an avatar - an image - associated with users. But rather than asking users to upload images and storing them on my servers, I opted to use Gravatar which is a Web based service that can be used in many applications and is supported by a large number of popular Web sites already. Ultimately it's a better choice for users who only have to sign up once to associate an email address with a Gravatar to be used on many sites.
Here's what the Gravatar image looks like on a message board message:
The Gravatar service simply returns an an image for a given email address and some other parameters that are encoded and point at the Gravatar.com site. As a developer you configure Gravatar by creating a URL and embedding that URL into an <img> control's src attribute.
Above you can see the image that is associated with my Email address. The cool thing about Gravatar - if they have a Gravatar account already - is that once users have provided an email address they don't have to do anything else to associate their gravatar image with your application. If they have a gravatar configured the image is shown. If not, a default image is shown.
If an email address is sent to Gravatar that doesn't exist you can either provide another URL to an alternate image (some sort of default that's appropriate for your app) or you can let Gravatar serve it's default icon which looks like this:
How it works
The Gravatar service works by calling a URL on their site and providing a few known parameters. An URL to retrieve an email address looks something like this:
http://www.gravatar.com/avatar/a4ec5092141a8649fe9d81527569a4c0?s=80&r=r
In an image control:
<img src="http://www.gravatar.com/avatar/a4ec5092141a8649fe9d81527569a4c0?s=80&r="
class="gravatar"
alt="Gratar Image based on email address" />
The components you need to send to get a Gravatar image are:
- The Email Address encoded with an MD5 Hash
- An image size
- A default image if the email address is not registered with
- A rating for the image (g, pg, r, x)
I've created a small reusable function that helps create Gravatar images in FoxPro. Using the function you can do:
? GravatarLink("rstrahl@west-wind.com",80)
? GravatarLink("invalid@west-wind.com",60,,"r")
Here's the code for the GravatarLink function:
************************************************************************
* GravatarLink
****************************************
*** Function: Creates an image URL for a given email address
*** that is registered with Gravatar.com.
***
*** Gravatar is a very popular avatar service that
*** requires only an email address to share a picture.
*** Used on many web sites so once you sign up your
*** picture will be used on many sites.
*** Assume:
*** Pass: lcEmail - Email Address
*** lnSize - Image Size (square) 60-80 is usually good
*** lcDefaultImage - Url to an image if no match is found
*** for email. Empty shows Gravatar's default
*** lcRating - g, pg, r, x (Default: pg)
*** Return: URL to the Gravatar image
************************************************************************
FUNCTION GravatarLink(lcEmail,lnSize,lcDefaultImage, lcRating)
LOCAL lcDefaultImage
IF EMPTY(lnSize)
lnSize = 80
ENDIF
IF !EMPTY(lcEmail)
lcHash = LOWER(STRCONV(HashMd5(lcEmail),15))
ELSE
lcHash = ""
ENDIF
IF EMPTY(lcDefaultImage)
*** Gravatar default image displays
lcDefaultImage = ""
ELSE
lcDefaultImage = "&d=" + UrlEncode(lcDefaultImage)
ENDIF
IF EMPTY(lcRating)
lcRating = "pg"
ENDIF
lcUrl = "http://www.gravatar.com/avatar/" + lcHash + "?" +;
"s=" + TRANSFORM(lnSize) +;
"&r=" + lcRating +;
lcDefaultImage
RETURN lcUrl
* GravatarLink
The code is pretty straight forward - it basically builds up a URL as a string and adds the components provided by the parameters. The trickiest part of this is the MD5 encoding of the Email address. The MD5 hash creates a binary value which is then turned into a HexBinary string with STRCONV().
In order to encode the email address as an MD5 hash I use the following routine based on the content from the FoxPro Wiki a long while back:
************************************************************************
* wwAPI :: HashMD5
****************************************
*** Function: retrieved from the FoxWiki
*** http://fox.wikis.com/wc.dll?fox~vfpmd5hashfunction
*** Assume: Self standing function - not part of wwAPI class
*** Pass: Data to encrypt
*** Return:
************************************************************************
FUNCTION HashMD5(tcData)
*** #include "c:\program files\microsoft visual foxpro 8\ffc\wincrypt.h"
#DEFINE dnPROV_RSA_FULL 1
#DEFINE dnCRYPT_VERIFYCONTEXT 0xF0000000
#DEFINE dnALG_CLASS_HASH BITLSHIFT(4,13)
#DEFINE dnALG_TYPE_ANY 0
#DEFINE dnALG_SID_MD5 3
#DEFINE dnCALG_MD5 BITOR(BITOR(dnALG_CLASS_HASH,dnALG_TYPE_ANY),dnALG_SID_MD5)
#DEFINE dnHP_HASHVAL 0x0002 && Hash value
LOCAL lnStatus, lnErr, lhProv, lhHashObject, lnDataSize, lcHashValue, lnHashSize
lhProv = 0
lhHashObject = 0
lnDataSize = LEN(tcData)
lcHashValue = REPLICATE(CHR(0), 16)
lnHashSize = LEN(lcHashValue)
DECLARE INTEGER GetLastError ;
IN win32api AS GetLastError
DECLARE INTEGER CryptAcquireContextA ;
IN WIN32API AS CryptAcquireContext ;
INTEGER @lhProvHandle, ;
STRING cContainer, ;
STRING cProvider, ;
INTEGER nProvType, ;
INTEGER nFlags
* load a crypto provider
lnStatus = CryptAcquireContext(@lhProv, 0, 0, dnPROV_RSA_FULL, dnCRYPT_VERIFYCONTEXT)
IF lnStatus = 0
THROW GetLastError()
ENDIF
DECLARE INTEGER CryptCreateHash ;
IN WIN32API AS CryptCreateHash ;
INTEGER hProviderHandle, ;
INTEGER nALG_ID, ;
INTEGER hKeyhandle, ;
INTEGER nFlags, ;
INTEGER @hCryptHashHandle
* create a hash object that uses MD5 algorithm
lnStatus = CryptCreateHash(lhProv, dnCALG_MD5, 0, 0, @lhHashObject)
IF lnStatus = 0
THROW GetLastError()
ENDIF
DECLARE INTEGER CryptHashData ;
IN WIN32API AS CryptHashData ;
INTEGER hHashHandle, ;
STRING @cData, ;
INTEGER nDataLen, ;
INTEGER nFlags
* add the input data to the hash object
lnStatus = CryptHashData(lhHashObject, tcData, lnDataSize, 0)
IF lnStatus = 0
THROW GetLastError()
ENDIF
DECLARE INTEGER CryptGetHashParam ;
IN WIN32API AS CryptGetHashParam ;
INTEGER hHashHandle, ;
INTEGER nParam, ;
STRING @cHashValue, ;
INTEGER @nHashSize, ;
INTEGER nFlags
* retrieve the hash value, if caller did not provide enough storage (16 bytes for MD5)
* this will fail with dnERROR_MORE_DATA and lnHashSize will contain needed storage size
lnStatus = CryptGetHashParam(lhHashObject, dnHP_HASHVAL, @lcHashValue, @lnHashSize, 0)
IF lnStatus = 0
THROW GetLastError()
ENDIF
DECLARE INTEGER CryptDestroyHash ;
IN WIN32API AS CryptDestroyHash;
INTEGER hKeyHandle
*** free the hash object
lnStatus = CryptDestroyHash(lhHashObject)
IF lnStatus = 0
THROW GetLastError()
ENDIF
DECLARE INTEGER CryptReleaseContext ;
IN WIN32API AS CryptReleaseContext ;
INTEGER hProvHandle, ;
INTEGER nReserved
*** release the crypto provider
lnStatus = CryptReleaseContext(lhProv, 0)
IF lnStatus = 0
THROW GetLastError()
ENDIF
RETURN lcHashValue
ENDFUNC
* HashMD5
With this function in place it's now a snap to embed Gravatar images into HTML based applications. In a Web Connection (or any other template/script based environment that uses Fox code) you can now simply do:
<img src="<%= GravatarLink(poUser.Email,80,,"pg") %>" class="gravatar" />
And you're off to the races.
On the West Wind MessageBoard
If you're a user of the West Wind Message Board I recommend you head over to Gravatar.com and hook up your Avatar image if you are interested in showing an image for any of your messages. Go ahead post a message, and see your Gravatar pop up. Once you have a Gravatar you may find that a lot of sites where you participate also already use Gravatar images so this will be useful in a lot of places beyond just the message board or your own applications.
GravatarLink() is now also part of the wwUtils library in Web Connection and the West Wind Client Tools.
Thursday, January 26, 2012, 3:19:19 PM
Overlayed bitmaps in FoxPro have always been a royal pain, but in order to make a UI that looks clean, getting some sort of transparency to work with FoxPro controls is pretty important. Although FoxPro supports most common image formats including GIF and PNG that support transparency, the transparency isn't supported everywhere within the product.
Specifically the FoxPro Image control supports transparency of the various transparent image formats. If you load up an image control with a GIF or PNG file, the transparency is preserved and the image displays correctly.
However, displaying transparent images on other controls that have a Picture property, unfortunately doesn't work as smoothly. Specifically, here's an example of what I've been struggling with, which is buttons with associated icons:
The first button doesn't show transparency and looks terrible, while the second button properly shows transparency and appears like it should using one of the approaches mentioned below. Similar situation arises with other controls that have picture properties like OptionButton, Checkboxes. ComboBoxes and Listboxes as well, although it's less of a problem there because the background of lists tend to be white. As a matter of fact all container controls have a picture property and the same rules apply.
By default only the Image control supports transparency properly for all GDI+ image formats.
The good news is that with a bit of trickery you can get FoxPro to render transparent images. The bad news is that there are tradeoffs and extra work required to make it work properly.
Old School Transparency Support: BMP Image
GDI+ image support for non-BMP images is actually a relatively new feature in FoxPro. GDI+ support for graphics was introduced with Visual FoxPro 8. Prior to version 8 only BMP images were directly supported in the product and there are a couple of mechanisms used for handling transparency with BMPs.
The most reliable way to get transparency in FoxPro involves using BMP files and a matching BMP Mask file. The mask file basically holds black dots for each of the pixels that should display and white content for anything that should be transparent. As you might imagine creating MASK files is a pain and adds clutter to your image management - anytime the image changes the mask has to be changed too and keeping things in sync is terrible. To me this has always been a non-starter.
You can also get transparency support with only BMP images. By default BMP images display white pixels as transparent, so as long as you can easily represent your transparent content as white it's easy to do. This is not always so cut and dried however because the image 'content' may also contain white pixels. In this case you can fill the white pixels with a slightly off white color like RGB(254,254,254).
Both of these approaches are painful but they are very reliable. Once you got your image set up it always works without fail. But both approaches require that you at the very least convert them to BMP format and potentially tweak your images for transparency or by creating a mask.
Resource Loading in FoxPro
There's another approach that works in most situations because of a quirk in FoxPro that I just found out about last week. FoxPro loads images as resources and you can trick FoxPro by loading images with transparency by first loading them into an Image control which as mentioned earlier does support transparency. The trick is that you have to load the image into the image control BEFORE it gets loaded into a picture property of another control. The quirk is that FoxPro caches image data once loaded from a path, so loading it into an image control first caches the transparent image which then gets loaded in subsequent loads that reference the same disk image (or compiled in image resource).
The image control has to only be loaded up and the control can then be released. The key is that this 'pre-load' has to happen BEFORE you load the same image into other controls. The image control doesn't need to stick around, so I use a small function that I only need to pass a file path to, create the Image control and set the picture property:
************************************************************************
* LoadImageResource
****************************************
*** Function: Pre-load an image file so transparency is respected
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION LoadImageResource(lcFile)
LOCAL loImg as Image
loImg = CREATEOBJECT("Image")
loImg.Picture = FULLPATH("bmps\search_small.png")
ENDFUNC
* LoadImageResource
When the function exits the Image control is released, but since the image was effectively loaded into the control the image is now cached inside of VFP's image cache. Now when you load an image that has been called with the function into a Picture property of another control like a button it will display GIF and PNG transparency properly.
In an application I tend to have quite a few image resources I need to load up, so centralize one place when I do all the image pre-loading during startup. I create a function that I call from the application's startup, typically right after I display a startup splash screen.
The method is simply a conglomeration of LoadImageResource() calls:
************************************************************************
* LoadImageResources
****************************************
*** Function: Method is used to load up transparent images into
*** UI. This method should be called on startup
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION LoadImageResources()
LOCAL loImg as Image
LoadImageResource("bmps\search_small.png")
LoadImageResource("bmps\zipfile.gif")
…
ENDFUNC
* LoadPictureResources
This is a simple approach that works fine and allows using PNG and GIF images with transparency on buttons and other controls.
Caveats with Preloading
In some instances however you can still end up with non-transparent resources. Basically FoxPro caches resources internally, but there are several ways that resources can get un-cached. Explicitly, if your code calls the CLEAR RESOURCES command anywhere, resources will get released and you'll lose any cached resources at that point. If you go back to a form with a transparent image on a control the image will display again with opaque background. Note that CLEAR RESOURCES is not affected by commands like CLEAR ALL or RELEASE ALL.
Implicitly, FoxPro can on certain occasions also unload its image resources internally when memory is really low. This should be very rare but it's possible and I have seen occasions when this does occur. But it's really rare so it's probably OK to ignore that possibility. If you're concerned about this scenario you can either have some code that calls LoadImageResources() or something like it during certain key application points that fire occasionally to explicitly force reloading of the images, or you can load resources as needed everytime a given form is fired up. This will ensure that resources are always fresh, but keep in mind that this causes some extra overhead.
Summary
I sure wish this was easier to accomplish or more obvious. For the longest time I didn't even know this hack of image pre-loading. The safest solution is to use BMP files, but it's definitely way more convenient to use Web ready files like PNGs or GIFs that support transparency both for designing the icons and re-using them across applications and platforms (Web/Desktop as I do).
Having to pre-load resources for transparent images in this fashion is a big hassle as opposed just loading up image resources directly. And worse it's an ugly hack that's impossible to discover on your own. I found a vague reference to this on a message board message somewhere so it's even hard to search for. Hopefully this blog entry will help clarfiy the subject a little better, even if it's just for me to remember in the future.
What really sucks about this problem is the fact that it DOES works with preloaded image resources. This seems to suggest that transparency actually works on controls but there's a small implementation detail in Visual FoxPro that prevents this from working all the time. It seems this would have been such a minor thing for the Fox team to fix and get working out of the box if the resources to do so had been there. But alas, we're stuck with a few minor and undiscoverable hacks. I take a hack over it not working at all which was what I thought until a week ago.
Thanks to Cesar Chalom, Joel Leach, Dragan Nedeljkovich for providing some ideas for this post and Mike Lewis for providing the inspiration to look further into this and getting me to a working solution in Html Help Builder.
Thursday, January 19, 2012, 3:56:39 AM
If you take a look at the Visual FoxPro documentation and the System Limits you find that FoxPro's string length limit is somewhere around ~16mb. The following is from the FoxPro Documentation:
Maximum # of characters per character string or memory variable: 16,777,184
Now 16mb seems like a lot of data, but in certain environments like Web applications it's not uncommon to send or receive data larger than 16 megs. In fact, last week I got a message from a user of our Client Tools lamenting the fact that the HTTP Upload functionality does not allow for uploads larger than 16 megs. One of his applications is trying to occasionally upload rather huge files to a server using our wwHttp class. At the time I did not have a good solution for him due to the 16meg limit.
What does the 16meg Limit really mean?
The FoxPro documentation actually is not quite accurate! You can actually get strings much larger than 16megs into FoxPro. For example you can load up a huge file like the Office 2010 download from MSDN like this:
lcFile = FILETOSTR("e:\downloads\en_office_professional_plus_2010_x86_515486.exe")
? LEN(lcFile)
The size of this string: 681,876,016 bytes or 681megs! Ok that's a little extreme :-) but to my surprise that worked just fine; you can load up a really huge string in VFP if you need to. But when you get over 16megs the behavior of strings changes and you can't do all the things you normally do with strings.
Other operations do not work for example, the following which creates an 18meg string fails:
lcString = REPLICATE("1234567890",1800000)
with "String is too long to fit".
However the following which creates a 25 meg string does work:
lcString = REPLICATE("1234567890",1500000)
lcString = lcString + REPLICATE("1234567890",1000000)
? LEN(lcString) && 25,000,000
The following is almost identical except it copies the longer string to another string which does not work:
lcString = REPLICATE("1234567890",1500000)
lcNewString = lcString + REPLICATE("1234567890",1000000)
And that my friends is the real sticking point with large strings. You can create them, but once they get bigger than 16megs you can no longer assign them to a new variable. That might sound easy to avoid but it's actually tough to do. If you pass string to methods it's very likely that they are actually copied into temporary variables or added to another variable in a simulated buffer, and that is typically where large strings fail.
So what can we learn from this:
Doesn't Work:
- Assigning a massive FoxPro string to another string fails
- Some FoxPro commands like REPLICATE() can't create output larger than 16 megs
Works:
- Assigning a massive string from a file using FileToString() works
- Adding to the same large string (lcOutput = lcOutput + " more text") works and the string can grow
- Calling methods that manipulate string work as long as the same string is assigned
There are some limitations but knowing that if you work with a single string instance that can grow large is actually good news. What this means is that if you're careful with how you use strings in FoxPro you can fairly easily get around the 16 meg string limit.
This actually worked well for me in the wwHttp class and the POST issue for larger than 16meg files but not string. Internally wwHttp uses a cPostBuffer property to hold the POST data. The failure was occuring in the send code which would copy the string to a temporary string and get the size, then pass that to the WinInet APIs. The fix for this was fairly easy: Rather than creating the temporary variables (which were redundant anyway) I simply used the class property directly throughout the code without any hand off and voila, now wwHttp supports POSTs for greater than 16 megs.
The code I use is kinda ugly because it's doing lots of string concatenation to build up the Post buffer. Something along these lines like this excerpt from wwHttp::AddPostKey:
************************************************************************
* wwHTTP :: AddPostKey
*********************************
*** Function: Adds POST variables to the HTTP request
*** Assume: depends on nHTTPPostMode setting
*** Pass:
*** Return:
************************************************************************
FUNCTION AddPostKey(tcKey, tcValue, llFileName)
LOCAL lcOldAlias
tcKey=IIF(VARTYPE(tcKey)="C",tcKey,"")
tcValue=IIF(VARTYPE(tcValue)="C",tcValue,"")
IF tcKey="RESET" OR PCOUNT() = 0
THIS.cPostBuffer = ""
RETURN
ENDIF
*** If we post a raw buffer swap parms
IF PCOUNT() < 2
tcValue = tcKey
tcKey = ""
ENDIF
IF !EMPTY(tcKey)
DO CASE
*** Url Encoded
CASE THIS.nhttppostmode = 1
THIS.cPostBuffer = this.cPostBuffer + IIF(!EMPTY(this.cPostBuffer),"&","") + ;
tcKey +"="+ URLEncode(tcValue)
*** Multi-part formvars and file
CASE this.nHttpPostMode = 2
*** Check for File Flag - HTTP File Upload - Second parm is filename
IF llFileName
THIS.cPostBuffer = THIS.cPostBuffer + "--" + MULTIPART_BOUNDARY + CRLF + ;
[Content-Disposition: form-data; name="]+tcKey+["; filename="] + JUSTFNAME(tcValue) + ["]+CRLF+CRLF
this.cPostBuffer = this.cPostBuffer + FILETOSTR(FULLPATH(tcValue))
this.cPostBuffer = this.cPostBuffer + CRLF
ELSE
this.cPostBuffer = this.cPostBuffer +"--" + MULTIPART_BOUNDARY + CRLF + ;
[Content-Disposition: form-data; name="]+tcKey+["]+CRLF+CRLF
this.cPostBuffer = this.cPostBuffer + tcValue
ENDIF
ENDCASE
ELSE
*** If there's no Key post the raw buffer
this.cPostBuffer = this.cPostBuffer +tcValue
ENDIF
ENDFUNC
AddPostKey can accept either a string value or a filename to load from. The file loading works by accepting the filename and then directly loading the file from within the function:
this.cPostBuffer = this.cPostBuffer + FILETOSTR(FULLPATH(tcValue))
This works fine because the file is directly loaded up into the buffer with no intermediate string variable.
You cannot however pass a string that is greater than 16 megs into this function because the code that adds the key basically does this with the tcValue parameter:
this.cPostBuffer = this.cPostBuffer + tcValue
which is assigning the larger than 16 meg string (tcValue in this case) to another variable and as discussed earlier that fails with "String too long to fit". Using a string to buffer your output to build up a larger string, there's no workaround for adding a larger than 16 meg string to another variable or buffer using variables. So my code now works with files loaded from disk, but not string parameters
Good but not good enough!
Files for Large Buffers
Based on the earlier examples I showed we know that we can easily load up massive content from a file. Thus FILETOSTR() offers an easy way to serve large files. Knowing that it's possible to build stream like class that allows you to accumulate string content in a file and then later retrieve it. To do this I created a wwFileStream class. Using the class looks like this:
*** Load library
DO wwapi
*** Create 20 meg string
lcString = REPLICATE("1234567890",1500000)
lcString = lcString + REPLICATE("1234567890",500000)
*** Create a stream
loStream = CREATEOBJECT("wwFileStream")
*** Write the 20meg string
loStream.Write(lcString)
*** Add some more string data
loStream.WriteLine("...added content")
*** Now write a 16meg+ to the buffer as well
loStream.WriteFile("e:\downloads\ActiveReports3_5100158.zip")
*** Works
lcLongString = loStream.ToString()
*** 55+ megs
? loStream.nLength
? LEN(lcLongString)
*** Clear the file (auto when released)
loStream.Dispose()
Using this mechanism you can build up very large strings from files or strings regardless of what the size of the string is.
How wwFileStream works
Internally wwFileStream opens a low level file and tracks the handle. Each Write() operation does an FWRITE() to disk and the handle is released when the class goes out of scope.
The class implementation is pretty straight forward:
*************************************************************
DEFINE CLASS wwFileStream AS Custom
*************************************************************
*: Author: Rick Strahl
*: (c) West Wind Technologies, 2012
*:Contact: http://www.west-wind.com
*:Created: 01/04/2012
*************************************************************
nHandle = 0
cFileName = ""
nLength = 0
************************************************************************
* Init
****************************************
FUNCTION Init()
this.cFileName = SYS(2023) + "\" + SYS(2015) + ".txt"
this.nHandle = FCREATE(this.cFileName)
this.nLength = 0
ENDFUNC
* Init
************************************************************************
* Destroy
****************************************
FUNCTION Destroy()
this.Dispose()
ENDFUNC
* Destroy
************************************************************************
* Dispose
****************************************
FUNCTION Dispose()
IF THIS.nHandle > 0
TRY
FCLOSE(this.nHandle)
DELETE FILE (this.cFileName)
CATCH
ENDTRY
ENDIF
this.nLength = 0
ENDFUNC
* Destroy
************************************************************************
* Write
****************************************
FUNCTION Write(lcContent)
THIS.nLength = THIS.nLength + LEN(lcContent)
FWRITE(this.nHandle,lcContent)
ENDFUNC
* Write
************************************************************************
* WriteLine
****************************************
FUNCTION WriteLine(lcContent)
this.Write(lcContent)
this.Write(CHR(13) + CHR(10))
ENDFUNC
* WriteLine
************************************************************************
* WriteFile
****************************************
FUNCTION WriteFile(lcFileName)
lcFileName = FULLPATH(lcFileName)
this.Write(FILETOSTR( lcFileName ))
ENDFUNC
* WriteFile
************************************************************************
* ToString()
****************************************
FUNCTION ToString()
LOCAL lcOutput
FCLOSE(this.nHandle)
lcOutput = FILETOSTR(this.cFileName)
*** Reopen the file
this.nHandle = FOPEN(this.cFileName,1)
FSEEK(this.nHandle,0,2)
RETURN lcOutput
ENDFUNC
* ToString()
************************************************************************
* Clear
****************************************
FUNCTION Clear()
THIS.Dispose()
THIS.Init()
ENDFUNC
* Clear
ENDDEFINE
*EOC wwFileStream
The code is fairly self explanatory. The class creates a file in the temp folder and saves the handle. Any write operation then uses the file handle to FWRITE() either a string or the output from FILETOSTR(). ToString() can be called to retrieve the file, which closes the file, reads it then reopens it and points to the end. When the class is released the handle is closed and the handle released.
Using this class makes it easy to create large strings and hold onto them. The additional advantage is that memory usage is kept low as strings are loaded up only briefly and then immediately written to file and can be released. So if you're dealing with very large strings a class like this is actually highly recommended. In fact Web Connection uses this same approach for file based application output.
A matching MemoryStream Class
While the FileStream class works, it does have some overhead compared to memory based operation especially when you're dealing with small amounts of data. In the wwHttp class for example, I would not want to create a new wwFileStream for each POST operation. 99% of POST ops are going to be light weight, so it makes sense to only use the wwFileStream class selectively.
In order to do this I also created a wwMemoryStream class which has the same interface as wwFileStream and which uses a simple string property on the class to hold data. Since the classes have the same interface they are interchangable in use which makes them easily swappable.
The code for wwMemoryStream looks like this:
*************************************************************
DEFINE CLASS wwMemoryStream AS Custom
*************************************************************
*: Author: Rick Strahl
*: (c) West Wind Technologies, 2012
*:Contact: http://www.west-wind.com
*:Created: 01/05/2012
*************************************************************
cOutput = ""
nLength = 0
************************************************************************
* Destroy
****************************************
FUNCTION Destroy()
THIS.Dispose()
ENDFUNC
* Destroy
************************************************************************
* Dispose
****************************************
FUNCTION Dispose()
this.cOutput = ""
this.nLength = 0
ENDFUNC
* Dispose
************************************************************************
* Clear
****************************************
FUNCTION Clear()
this.cOutput = ""
this.nLength = 0
ENDFUNC
* Clear
************************************************************************
* Write
****************************************
FUNCTION Write(lcContent)
this.nLength = this.nLength + LEN(lcContent)
this.cOutput = this.cOutput + lcContent
ENDFUNC
* Write
************************************************************************
* WriteLine
****************************************
FUNCTION WriteLine(lcContent)
this.Write(lcContent)
this.Write(CRLF)
ENDFUNC
* WriteLine
************************************************************************
* WriteFile
****************************************
FUNCTION WriteFile(lcFileName)
this.Write(FILETOSTR( FULLPATH(lcFileName) ))
ENDFUNC
* WriteFile
************************************************************************
* ToString()
****************************************
FUNCTION ToString()
RETURN this.cOutput
ENDFUNC
* ToString()
ENDDEFINE
*EOC wwMemoryStream
It's now a cinch to create my class depending on the need.
In wwHttp I use wwMemoryStream by default since it addresses the 99% scenario. I added two properties to wwHttp: oPostStream and cPostStreamClass. The class is set to wwMemoryStream which is the default and can be overridden. Then internally when the time comes to create an instance of the stream class I use:
IF VARTYPE(this.oPostStream) != "O"
this.oPostStream = CREATEOBJECT(this.cPostStreamClass)
ENDIF
This way the user can easily chose which of the streams to use simply by specifying:
loHttp.cPostStreamClass = "wwFileStream"
What's also nice about this approach is that the mechanism becomes extensible. If you want to store POST vars in another storage format you can simply create another subclass that implements the same methods and now can store your post variables in an INI file or in structured storage etc. Unlikely scenario for POST data, but very useful for other potential data storage scenarios.
BTW, the wwFileStream class is also a fairly useful generic file output tool. If you ever need to write output to files it provides a real easy OO way to do so, cleaning up after itself when you close it. I've used classes like (wwResponseFile) for years in various applications that need to create file output. It's very useful in many situations.
Summary
Even though Visual FoxPro has a 16 meg string limit, you now have some tools in your arsenal to work around this limit and work with larger strings. While you can work with larger strings, keep in mind that once you go past 16 megs you can't assign that string to anything else. It also gets much harder (and slower) to string manipulation on that string once you're beyond VFP's legal limit.
Still it's nice to know that the limit is not a final one and there are ways to work around it.
Resources
Wednesday, January 04, 2012, 5:20:14 AM
Ran into a little nit today in some code for Help Builder. I have a small dialog in my toolbox that I use to create HREF links. Basically this dialog captures a title, URL and either returns those values back or creates an anchor (<a>) HTML tag from it.
The dialog looks like this:
Usually a user will be editing some help topic, then type some text like Visit our Support Site, highlight the selection of text and then pop into the dialog. Ideally I want to start out in the Web Link field and have the text highlighted.
So in the designer I have thisform.txtUrl.Format = "K" which autoselects the text in both controls. Now this all works fine as long as I tab through the controls manually. However, my original code used the original TabOrder and tried to SetFocus to the URL like this in the Init of the form class:
LPARAMETER lcUrl, lcText
THIS.cUrl = TRIM(lcUrl)
THIS.cText = lcText
*** Move focus to second field
IF !EMPTY(lcText)
thisform.txtURL.SetFocus()
*KEYBOARD "{TAB}"
ENDIF
This works fine for moving the focus - the control comes up with the focus set to txtUrl just fine. But unfortunately the @K auto-selection of the text is not happening. Using SetFocus() during the startup of the form simply fails. It only fails when using SetFocus() during startup in Init() or before the form is activated - if you call SetFocus() after activation the @K selection is properly respected.
Workarounds
Of course there's a simple and obvious workaround that probably would have been the better solution in the first place here. I can simply set the TabOrder to force the txtUrl field to be the first field accessed by the form and adjust the taborder accordingly with the txtTitle being the last in the taborder.
If you really need dynamic focusing at runtime, you can move that code to a later time in the form's event cycle: Moving it to the Activate event works.
Saturday, November 26, 2011, 2:47:25 PM
As you probably know Visual FoxPro projects can be compiled into COM servers. It's as easy as creating a new FoxPro class and marking it with the OLEPUBLIC keyword to allow it to be compiled into a COM Server.
In a PRG file it's as easy as creating a class like this:
DEFINE CLASS SimpleClass as Session OLEPUBLIC
FUNCTION HelloWorld(lcName)
RETURN "Hello " + lcName
ENDDEFINE
Now add this class to a project called SimpleServer and compile the class either into an EXE (which becomes a DCOM out of process server) or a DLL (MTDLL or Regular DLL).
BUILD MTDLL SimpleServer FROM simpleserver
and voila you have a COM server that you can now instantiate from other environments or even from Visual FoxPro.
o = CREATEOBJECTEX("SimpleServer.SimpleClass","")
? o.HelloWorld("Rick")
The name of the COM server by default becomes:
ProjectName.ClassName
so here the project is SimpleServer and the class is SimpleClass.
Note I'm using CreateObjectEx here to force VFP to use the object as a COM object in VFP - in some situations VFP treats in process (DLL) FoxPro COM objects like Fox objects and CREATEOBJECTEX() ensures that the object is always loaded as a true COM object. Of course you can do something similar in VB classic, or .NET, Delphi, C++ etc. where use a FoxPro COM object makes a lot more sense than inside of FoxPro code where you could (and should if possible) use the object natively.
When you use a DLL server as above the COM instance will be loaded into the host process - it's an InProcess component and it runs in the hosts environment. This means that the environment settings (Startup Path, memory setup, OS Paths etc) are all inherited from the host process inside of the DLL server. In effect the DLL server becomes part of the host process just like any other DLL loaded into it.
To create an EXE server you can just compile the same code into an EXE:
BUILD EXE SimpleServer FROM simpleserver
Once you've compiled an EXE you can use the same code used previously to instantiate the server - nothing changes. However, now the server is an out of process server and runs in its own process - SimpleServer.exe if you look it up in Task Manager - that is completely separate from the host process. SimpleServer.exe gets instantiated by the Visual FoxPro runtime and it gets its own brand new environment and any interaction between the host application and your COM server occurs over an RPC Remoting interface that performs interprocess communication.
ClassIDs in Visual FoxPro
Typically you interact with COM objects via ProgIds like SimpleServer.SimpleClass, but internally all COM access is routed through unique GUID identifiers called ClassIDs. To find more info about COM servers you can use the COMCLASSINFO function in FoxPro. One of the options of COMCLASSINFO is option 4:
o = CREATEOBJECT("SimpleServer.SimpleClass")
? COMCLASSINFO(o,4)
which shows you the ClassId for the COM object which looks like this (but will be different if you were following along - GUIDs are unique):
{6C552462-832E-432A-9059-351145E7090B}
Visual FoxPro creates these unique CLASSIDs inside of the Project file. When you build your project VFP scans for any COM objects - those classes marked with OLEPUBLIC - and keeps track of them. The first time a given class is encountered VFP stores its GUID inside of the project file. Subsequent builds retrieve this GUID and reuse it for registration and creation of the type library and DllRegister/DllUnregister functions that compile into VFP.
Specifically it stores this data - ProgId and ClassIds in the Reserved 2 field of the PJX header record. To demonstrate I added a second COM class (SimpleServerNo2) to the project and compiled. When I then open USE SimpleServer.pjx and BROWSE I can see the Reserved2 field which looks like this:
4 103 4 2 30c:\wwapps\wc3\SimpleServer.DLL 12simpleserver 12simpleserver 25simpleserver Type Library 4 116 38{F10346E2-C9D9-47F7-81D1-74059CC15C3C} 10 24simpleserver.SimpleClass 24simpleserver.SimpleClass 0 11SimpleClass 30c:\wwapps\wc3\simpleserver.prg 12 38{6C552462-832E-432A-9059-351145E7090B} 38{3753AF24-8DCA-4D00-B59B-3CA256C07091} 10 27simpleserver.SimpleClassNo2 27simpleserver.SimpleClassNo2 0 14SimpleClassNo2 30c:\wwapps\wc3\simpleserver.prg 12 38{353EB458-7DDE-4987-8BC0-42AD9ECCB9E9} 38{EDBC795C-62AB-42F6-863B-45708C680C72}
What you see here is a fixed length format record of project information. Most of this info comes from the Project Info dialog in FoxPro. But some information - namely the COM Server configuration additional 'records' are added to the Reserved2 field. I marked the ClassIds for the two COM servers in italic in the string above. Each COM server has both a CLASSID as well as an (IID) Interface ID which is the second classID you see following the ClassID.
Why does this matter?
If you corrupt a project in some way you CAN, if you're really desperate, fix things by going in here and fixing up the ProgIds or ClassIds to match known values. I've had many occasions where customers have copied around projects multiple times and have renamed classes without paying attention. Going into this field I've been able to 'save' the project and existing ClassIDs and ProgIds. Basically the COM objects are at the end
COM Information Corruption in FoxPro Projects
As a vendor who sells several FoxPro products that utilize COM to interact with other host environments I've seen many problems with FoxPro COM servers. One of the things I've seen a lot of is that COM CLASSIDs get corrupted.
Wrong Server loading
The most common reason for this corruption that I've seen comes from projects being copied. Since VFP stores COM information as part of the project file, copying the project (ie. COPY FILES SimpleServer.pj* to SimpleServer2.pj*) copies all the COM information as well. Sometimes this is what you want, other times not. If you want to make an exact copy of a project for testing or backup then project copying is totally fine. Just keep in mind that if you build the copied project it will share the same ProgIds and ClassIds as the original project and when you build this second project all COM references will point at this DLL.
Often times, however, people copy projects because they want a new starting point for a new project. They copy the project and various files not realizing they are essentially copying the COM functionality as well. What ends up happening is that two different objects now exist in two separate projects that are registered with the same ClassIDs. The result of this is that one registration overwrites the other and if the two versions are not kept in sync unpredictable behavior ensues because you're never sure which type of object gets loaded.
Moral of this story is: When you copy projects with COM objects for creation of a new project: Always create a new project and import all files from scratch. This ensures that each project gets their own set of CLASSIDs.
Which server is which?
When in doubt which server holds what COM object it's best to manually register projects. To be sure always register the component you're interested in manually with:
regsvr32 SimpleServer.dll
or for an EXE:
SimpleServer.exe /regserver
This always makes sure you get the right server loaded.
ClassID Corruption
When VFP builds a project it checks the ClassID in the project and tries to find existing classids in the registry. VFP checks the validity of the entries in the registry and tries to update the registry with the latest build information. VFP is pretty smart about this and corruption of these IDs is very, very rare, but it does happen. Usually it happens when you have two projects where each was created on its own (ie. you didn't copy the projects). What happens in this scenario is that both projects reference the same ProgIds but different ClassIDs. In effect each project builds different registry entries. Aside from expected problems that each time the project is compiled in a different project it will end up pointing to a different binary on disk, this scenario can also cause corruption as VFP tries to update the registry. The error that comes up in this case is: "Invalid subscript reference" while trying to build the project when it's trying to register the COM server.
The first thing to try in this case is to see if you can find an old copy of your COM binary (DLL or EXE) and try to register it.
regsvr32 simpleserver.dll
or for an EXE server:
SimpleServer.exe /regserver
Re-registering might clean up the registry and point at the right locations for things to start working again. Once registered try loading the COM object. If that works, unload it and then try to rebuild your project, chances are good that now it will build.
If that fails, you can try and fix your project using the Reserved2 field mentioned before. It's all text so you can change values in this field, but be really careful - the field data is fixed length, so any character you change needs to be replaced with another (ie. spaces are important!). You can fix ProgIDs and ClassIds and paths if necessary.
If that fails another - more laborious option - is to clean the registry of your COM object.
- Find all instances of your ProgId (s) HKCR\simpleserver.SimpleClass HKCR\simpleServer.SimpleClassNo2
- Find all instances of your TypeLib HKCR\TypeLib\{guid}
- Find all instances of your servers executable name (ie. SimpleServer.dll)
In the case of my SimpleServer example I'd just search the registry for "SimpleServer". Delete all the root entries that you find for matches (ie. walk them back up to the GUID entry or the ProgId entry). This will be the ProgId key in the root and the various ClassId entries in the All key.
If your name is not clearly delineated you have to search more specifically on the ProgId and the ClassId to find all entries - both ways work, but the latter is more tedious.
Once the registry is clean make sure you only compile the same COM objects from a single project OR if you need to have multiple projects with same COM objects, make 100% sure that the second project is a copy of the original project that contains all the same ClassID entries!
When in doubt: Create a new Project with a New Name
Your last resort option that should always work is to create a brand new project with a new name. Don't copy it, just create a new project and give it a new name and then add all your project files back to the project. When you create a new project with a new name the ProgIds will change as will the ClassIds. Essentially creating a new project with a new name ensures that you're starting from scratch and it's almost guaranteed to work. The downside of course is that all your COM ProgIds change and any client code that's already using your COM objects has to change.
I've had to do this in the past with a few projects that were corrupted and also ended up corrupting a customer's installations. Starting over was really the only option to make this all work out properly. However, when you're down to your last resort this is the most reliable solution to get to a known good starting point.
Not as bad as it sounds
If all this sounds scary, don't worry. A lot of what I'm describing here is really, really rare. But it does happen occasionally. If you are careful with copying projects then you are almost sure to never run into a problem. Just make sure you understand what happens when you copy a project that has existing COM objects in it. Safest route is to create a new project, with a new name and start fresh - this is guaranteed to avoid COM object corruption.
Monday, November 21, 2011, 11:53:00 AM
For a while I've been slacking on my FoxPro blogging. Part of the reason has been that I'm spoiled - spoiled by using Windows Live Writer on my main Weblog (where I post most of my .NET and general content). In case you're a blogger and you haven't used Live Writer, I urge you to RUN not walk over to the Windows Live site and download a copy. Seriously this will make your life much easier. With a couple of plug-ins - namely my SnagIt Screen Capture plug-in and the Paste from Visual Studio (which captures RTF colored text including with some limitations FoxPro code)
My main site's Weblog runs a custom-built ASP.NET application that built years ago and I built a MetawebLog API provider for it so it would work easily with Live Write (and other blogging tools). MetawebLog API is a standard format that can be used to post post information from the client to the server. The API is about 10 methods that implement things like retrieving blogs (for multi-blog hosts), retrieving recent posts, individual posts, deleting posts and of course allow you to post and update existing posts as well as handling binary/image uploads. One thing that's really painful about MetaWebLogAPI is that it's built using the Xml-RPC protocol which is an old and rarely used API. It would have been a lot easier if a generic SOAP or JSON API existed to do this. Alas, we're stuck with MetaWeblogAPI since that's what most editors suppport (as well as WordPress, Blogger and a few other specific formats).
Anyway, I wrote some .NET code a long while back to implement MetaWebLog API in my main blog and that's been working great. However, working on my FoxPro blog which is written with FoxPro code and Web Connection, I've been suffering from feature envy. For the Fox blog I used to use Word and then pasted content into a rich edit box - yuk! Later on I finally got wise and I used Live Writer to write my posts for my main blog, and post the FoxPro entries as Drafts. I'd then pick up the raw HTML and paste it into the FoxPro blogs entry form. Yeah, that sucked too.
So finally today I said - enough of this and sat down to create a provider to work against my Fox data in the FoxPro blog. Now I cheated a bit here - rather than using FoxPro and the Web Connection base code and reinvent XmlRpc and then build the MetaWeblog API ontop of it, I instead used my existing .NET MetawebLog base classes and recreated the final implementation talking to FoxPro data with OleDb. That's not something I like to do much - in fact I think this is the first ever application I've built for myself that uses OleDb against Fox data, but in the grand scheme of things this was simply the quickest way to go. It took me two hours to build the following implementation.
DotNet and FoxPro Data
In the past I've gotten a lot of questions about how to access FoxPro data from .NET. Using FoxPro data from .NET is just about limited to using ADO.NET which in raw form is very verbose - much more so than FoxPro's concise Data Definition Language. A long while back I created a really basic Data Access Layer (DAL) that can be used with any SQL backend and it provides highlevel abstractions that make basic SQL commands single method calls which is a lot less verbose than low level ADO.NET code.
For the following code I use the low level SqlDataAccess helper class which is available as part of the West Wind Web Toolkit and the Westwind.Utilities .NET assembly. Using this class here's what a few data access commands look like:
SqlDataAccess Data =
new SqlDataAccess(ConfigurationManager.ConnectionStrings["WebLogFox"].ConnectionString,
"System.Data.OleDb");
DataTable table = Data.ExecuteTable("TRecentEntries",
"select top 25 * from blog_entries order by entered desc");
// DataReaders are more efficient
DbDataReader reader = Data.ExecuteReader("select * from blog_entries where pk=?",
Data.CreateParameter("?",pk) );
int maxPk = (int)Data.ExecuteScalar("select max(pk) from blog_Entries");
int affected = Data.ExecuteNonQuery("insert into blog_user (pk, username, password) values (?,?)",
Data.CreateParameter("?","rick"),
Data.CreateParameter("?","sekrit") );
As you can see, it's not too difficult to make database calls - it looks a lot like using SQL Passthrough in FoxPro code which is no accident. For down and dirty, quick data access this stuff is sweet and easy and I use it frequently for administrative task - like say posting blog entries to my site.
Without much further ado (lower case! And the pun is intended) here's the code for my FoxPro based MetaWeblog implementation which puts the DAL above to use:
public class MetaWebLogApi : XmlRpcService, IMetaWeblog
{
private const string STR_RickStrahl = "Rick Strahl";
private const string STR_WconnectweblogimageContent = "~/weblog/imageContent/";
private const string STR_WebLogName = "Rick Strahl's FoxPro and Web Connection Weblog";
private static string STR_WeblogBaseUrl =
"http://" + HttpContext.Current.Request.ServerVariables["server_name"] +
"/wconnect/weblog/";
public SqlDataAccess Data = new SqlDataAccess(ConfigurationManager.ConnectionStrings["WebLogFox"].ConnectionString,
"System.Data.OleDb");
/// <summary>
/// Validates the user and throws exception on failure which will throw
/// us out of any service method and return the error to the client.
/// </summary>
/// <param name="Username"></param>
/// <param name="Password"></param>
/// <returns></returns>
private bool ValidateUser(string Username, string Password)
{
object val = Data.ExecuteScalar("select pk from weblogusersecurity where username==? and password=?",
Data.CreateParameter("?", Username),
Data.CreateParameter("?", Password));
if (val == null)
return false;
return true;
}
#region IMetaWeblog Members
public CategoryInfo[] getCategories(object blogid, string username, string password)
{
this.ValidateUser(username, password);
List<CategoryInfo> CategoryInfoList = new List<CategoryInfo>();
CategoryInfo cat = new CategoryInfo();
cat.categoryid = "1" ;
cat.description = "FoxPro";
cat.title = "FoxPro";
CategoryInfoList.Add(cat);
return CategoryInfoList.ToArray();
}
public Post getPost(string postid, string username, string password)
{
this.ValidateUser(username, password);
int Pk = 0;
if (!int.TryParse(postid,out Pk))
throw new XmlRpcException("Invalid PostId passed");
DbDataReader reader = Data.ExecuteReader("select * from blog_entries where pk = " + Pk.ToString());
if (reader == null || !reader.HasRows )
throw new XmlRpcException("invalid Post - no matching post found for id: " + Pk.ToString());
Post post = new Post();
while (reader.Read())
{
post.title = (string)reader["Title"];
post.description = (string)reader["body"];
post.postid = (int)reader["Pk"];
// *** Move to business object
post.permalink = STR_WeblogBaseUrl + "ShowPost.blog?id=" + Pk.ToString();
post.link = post.permalink;
post.dateCreated = (DateTime)reader["Entered"];
post.categories = reader["Categories"].ToString().Split(',');
post.mt_keywords = "";
post.mt_excerpt = (string)reader["abstract"];
}
return post;
}
public Post[] getRecentPosts(object blogid, string username, string password, int numberOfPosts)
{
this.ValidateUser(username, password);
DbDataReader reader = Data.ExecuteReader("select TOP ? * from blog_Entries order by entered desc",
Data.CreateParameter("?",numberOfPosts));
if (reader == null || !reader.HasRows)
throw new XmlRpcException("Error retrieving posts: " + Data.ErrorMessage);
List<Post> Posts = new List<Post>();
while (reader.Read())
{
Post post = new Post();
post.description = reader["body"].ToString();
post.title = reader["title"].ToString();
post.postid = (int)reader["pk"];
// *** Move to business object
post.permalink = STR_WeblogBaseUrl + "ShowPost.blog?id=" + reader["pk"].ToString();
post.link = post.permalink;
post.dateCreated = (DateTime) reader["Entered"];
string[] Categories = reader["Categories"].ToString().Split(new char[1] {','},StringSplitOptions.RemoveEmptyEntries);
post.categories = Categories;
Posts.Add(post);
}
return Posts.ToArray();
}
public bool editPost(string postid, string username, string password, Post post, bool publish)
{
this.ValidateUser(username, password);
int id = -1;
int.TryParse(postid, out id);
string sql =
@"update blog_entries
set Title=?,
body=?,
abstract=?,
active=?,
categories=?,
keywords=?
where pk = ?";
string abstr = post.mt_excerpt;
if (string.IsNullOrEmpty(abstr))
abstr = StringUtils.HtmlAbstract(post.description, 200);
string kwrds = post.mt_keywords;
if (string.IsNullOrEmpty(kwrds))
kwrds = string.Empty;
string userid = post.userid;
if (string.IsNullOrEmpty(userid))
userid = STR_RickStrahl;
string cats = "";
if (post.categories != null || post.categories.Length > 0)
{
foreach (string cat in post.categories)
cats += cat + ",";
cats.TrimEnd(',');
}
int res =
Data.ExecuteNonQuery(sql,
Data.CreateParameter("?", post.title),
Data.CreateParameter("?", post.description),
Data.CreateParameter("?", abstr),
Data.CreateParameter("?", publish),
Data.CreateParameter("?", cats),
Data.CreateParameter("?", kwrds),
Data.CreateParameter("?", id)
);
if (res == -1)
throw new XmlRpcException("Error inserting data: " + Data.ErrorMessage);
return true;
}
public string newPost(object blogid, string username, string password, Post post, bool publish)
{
this.ValidateUser(username, password);
int id = (int)Data.ExecuteScalar("select max(pk) from blog_entries");
id++;
string sql =
@"insert into blog_entries (pk,blogPk,Title, body, abstract, entered, updated, author,active,categories,keywords,feedback,url,BodyMode)
values (?,0,?,?,?,?,?,?,?,?,?,0,'',0)";
string abstr = post.mt_excerpt;
if (string.IsNullOrEmpty(abstr))
abstr = StringUtils.HtmlAbstract(post.description, 200);
string kwrds = post.mt_keywords;
if (string.IsNullOrEmpty(kwrds))
kwrds = string.Empty;
string userid = post.userid;
if (string.IsNullOrEmpty(userid))
userid = STR_RickStrahl;
string cats = "";
if (post.categories != null || post.categories.Length > 0)
{
foreach (string cat in post.categories)
cats += cat + ",";
cats.TrimEnd(',');
}
int res =
Data.ExecuteNonQuery(sql,
Data.CreateParameter("?",id),
Data.CreateParameter("?",post.title),
Data.CreateParameter("?",post.description),
Data.CreateParameter("?",abstr),
Data.CreateParameter("?",post.dateCreated < new DateTime(2010,1,1) ? DateTime.Now : post.dateCreated),
Data.CreateParameter("?", DateTime.Now),
Data.CreateParameter("?",userid),
Data.CreateParameter("?",publish),
Data.CreateParameter("?",cats),
Data.CreateParameter("?",kwrds)
);
if (res == -1)
throw new XmlRpcException("Error inserting data: " + Data.ErrorMessage);
return id.ToString();
}
public mediaObjectInfo newMediaObject(object blogid, string username, string password, mediaObject mediaobject)
{
this.ValidateUser(username,password);
string relPath = STR_WconnectweblogimageContent + DateTime.Now.Year.ToString() + "/";
string ImagePhysicalPath = HttpContext.Current.Server.MapPath(relPath);
if (Directory.Exists(ImagePhysicalPath))
Directory.CreateDirectory(ImagePhysicalPath);
string ImageWebPath = WebUtils.ResolveServerUrl(relPath,false);
if (mediaobject.bits != null)
{
MemoryStream ms = new MemoryStream(mediaobject.bits);
Bitmap bitmap = new Bitmap(ms);
ImagePhysicalPath = ImagePhysicalPath + mediaobject.name;
string PathOnly = Path.GetDirectoryName(ImagePhysicalPath).Replace("/","\\");
if (!Directory.Exists(PathOnly))
Directory.CreateDirectory(PathOnly);
bitmap.Save(ImagePhysicalPath);
}
mediaObjectInfo mediaInfo = new mediaObjectInfo();
mediaInfo.url = ImageWebPath + mediaobject.name;
return mediaInfo;
}
public bool deletePost(string appKey, string postid, string username, string password, bool publish)
{
this.ValidateUser(username, password);
int postpk = -1;
if (!int.TryParse(postid, out postpk))
throw new XmlRpcException("Invalid Pk passed");
if (Data.ExecuteNonQuery("delete from entries where pk = ?",
Data.CreateParameter("?", postpk)) > -1)
return true;
return false;
}
public BlogInfo[] getUsersBlogs(string appKey, string username, string password)
{
if (!this.ValidateUser(username, password))
return null;
BlogInfo blog = new BlogInfo();
blog.blogid = "0";
blog.blogName = STR_WebLogName;
blog.url = STR_WeblogBaseUrl;
return new BlogInfo[1] { blog };
}
public wp_author[] wp_getAuthors(object blogId, string username, string password)
{
this.ValidateUser(username, password);
wp_author author = new wp_author();
author.display_name = STR_RickStrahl;
author.user_email = "";
author.user_login = "";
author.meta_value = "";
return new wp_author[1] { author };
}
#endregion
}
This code is based on my original post which includes the XmlRpc class and IMetaWeblogApi implementation. The nice thing about this is that the process of converting this code to run with FoxPro data was pretty easy. The ASP.NET blog uses more sophisticated business objects since the entire app is running in .NET. The FoxPro app also uses business objects running against FoxPro data, but those business objects are not easily accessible to .NET. I wanted to avoid COM Interop at all costs here so I chose to go the direct SQL route.
I'm posting this here mainly because I've seen a lot of questions FoxPro about data choices. Here's an example that shows how to use a light abstraction layer around the ADO.NET API which goes a long way towards making it pretty easy to access FoxPro data with minimal fuss. You can use my data access layer (it's free for personal use and testing, and cheap for commercial use) or you can build something similar on your own.
And hey - if you've read this far you can see the fruits of this labor in code above: This post was posted to the blog using the metaweblog api client and Windows Live Writer. Feeling a little déjà vu, when viewing this screen shot? :-)
![LiveWriterEditing[10] LiveWriterEditing[10]](http://www.west-wind.com/wconnect/weblog/imageContent/2011/Windows-Live-Writer/ef2d63d29879_1A31/LiveWriterEditing%5B10%5D_thumb_1.png)
Resources
Thursday, November 17, 2011, 8:02:11 PM
So you're doing a bunch of COM Interop with .NET. Whether you're using plain COM Interop or the richer functionality offered by wwDotnetBridge, you probably already know that finding the approriate .NET type names is tedious. FoxPro code in either case must reference the full .NET typename which includes the namespace and class name.
For example check out this code that loops through personal certificate store on the local machine using wwDotnetBridge. I've highlighted all the .NET type names used in this small snippet in bold:
do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = CreateObject("wwDotNetBridge","V4")
*** Create an instance of 509Store
loStore = loBridge.CreateInstance("System.Security.Cryptography.X509Certificates.X509Store")
*** Grab a static Enum value
leReadOnly = loBridge.GetEnumvalue("System.Security.Cryptography.X509Certificates.OpenFlags","ReadOnly")
*** Open the certificate store - results into loStore.Certificates
loStore.Open(leReadOnly)
*** Collection of Certificates
laCertificates = loStore.Certificates
*** Collections don't work over regular COM Interop
*** so use indirect access
lnCount = loBridge.GetProperty(laCertificates,"Count")
*** Loop through Certificates
FOR lnX = 1 TO lnCount -1
*** Access collection item indirectly
LOCAL loCertificate as System.Security.Cryptography.X509Certificates.X509Certificate2
loCertificate = loBridge.GetPropertyEx(loStore,"Certificates[" + TRANSFORM(lnX) + "]")
IF !ISNULL(loCertificate)
? loCertificate.FriendlyName
? loCertificate.SerialNumber
? loBridge.GetPropertyEx(loCertificate,"IssuerName.Name")
ENDIF
ENDFOR
RETURN
How the heck do you figure out all those type names used in this code?
Answer: You use a .NET disassembling tool that provides insight into a .NET assembly. Regardless of whether you're using a system component or custom .NET component of your own being able to look up exact type names is critical if you do .NET COM Interop.
.NET Reflector
My favorite tool to do this is Red Gate's .NET Reflector. .NET Reflector is free up to version 6.8 and a relatively cheap tool for all that it provides from version 7.0 forward. All the features described here work in 6.x although I'm using version 7.
Using Reflector basically allows you to point at any DLL on disk and show all the contained types and resources that are available. It can also load assemblies from the GAC and by default it loads up a number of common .NET runtime components.
Here's a view of Reflector with the X509Store class open and selected:
Reflector provides a number of important pieces of information for Interop. It basically lets you browser the .NET component that you might want to access and call methods or access properties on. You can at a glance see what the component does and how you need to interact with it.
The next important thing is that you can get the full class name for any type. Notice in the bottom left window it gives the name of the type, its definition and the fully qualified type name that includes the full namespace and class. THIS is the type name you generally need for the wwDotnetBridge CreateInstance call.
loStore = loBridge.CreateInstance("System.Security.Cryptography.X509Certificates.X509Store")
I can't stress how important this particular piece of information is because while you might know what the name of a particular .NET class is often it's not so easy to figure out what the full name is. This is especially true if you're loading a third party assembly that might not be well documented. .NET components frequently omit detailed full type information documentation because .NET developers are used to Intellisense and Visual Studio helping them inject assembly and namespace references into their code. As FoxPro developers doing interop we don't have that luxury so for us using a tool like Reflector is vitally important.
Next you can also get detailed information about the methods that you call in .NET. Again this is vitally important as you have to ensure that you are passing the right parameters to .NET and that these types match the .NET signature. Type marshalling from FoxPro -> .NET doesn't always work as expected especially when dealing with numeric values (don't forget to use CAST()).
For the method view we get the full parameter signature plus a disassembled view of the actual code:
The disassembled code is very useful for .NET developers at times. As Fox developers all we care about is the method signature though. We can see all the parameters that are passed and all complex types (like the OpenFlags Enum here) are linked, so you can click on the link and directly navigate to the Enum type:
Now what's cool here is that you get to see the actual typename as before and we can now use wwDotnetBridge's Enum functionality to create the enum value in FoxPro code:
leReadOnly = loBridge.GetEnumvalue("System.Security.Cryptography.X509Certificates.OpenFlags","ReadOnly")
loStore.Open(leReadOnly)
You can see that I need the full type name here, and I can get that from the Name property in the window on the lower left.
However, there's actually an easier way: Since Reflector shows me the disassembled code it also shows me the Flag values for this particular enumeration. I can actually bypass the enumeration and just use the numeric value:
loStore.Open(0) && leReadOnly)
and it works just the same. Note that this will work as long the Enum is actually a flag value - not all enumerated values in .NET translate to a flag value so not all enums translated into numbers. You'll know because Reflector won't show the numbers if that's the case.
What's that Assembly Name from the GAC?
When loading assemblies with wwDotnetBridge from the GAC you need to specify a full assembly name rather than a filename. Actually you can use filename if you know the exact path in the GAC, but that's usually a rather long and obscure path. A better way to reference GAC components is via their full assembly name. For example earlier I needed to work with the .NET JavaScript serializer for long string serialization/deserialization. Here's a small code example that demon hat does this using wwDotNetBridge:
*** Load a GAC assembly (full .NET type name required)
loBridge.LoadAssembly("System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
*** Load a private assembly from disk by full file name
* ? loBridge.LoadAssemblY("c:\apps\myapp\myappassembly.dll")
*** Create an instance of the JavaScript Serializer
loSer = loBridge.CreateInstance("System.Web.Script.Serialization.JavaScriptSerializer")
lcValue = "Hello World\r\nHere's what I've got!"
*** Serialize string to JSON
lcJson = loSer.serialize(lcValue)
? lcJson
*** Create a .NET Type object for string
loType = loBridge.GetType("x")
*** Deserialize with the .NET type provided
? loSer.Deserialize(lcJson,loType)
To find this assembly in Reflector from the GAC I can use File | Open Global Assembly Cache and then look at the assembly in the list view:
And now the strong assembly name shows up in the name property on the bottom which I can then use in my LoadAssembly call:
loBridge.LoadAssembly("System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
Note with GAC components - especially those from Microsoft - the hardest part often is to figure which assembly components live in. When in doubt punch in a search into your favorite search engine and look at MSDN entries. As ineffectual as MSDN documentation often is, it does provide you with the necessary type information most of the time:
You get the two important pieces of information you need: The assembly the component lives in and the full namespace. Once you load the assembly you can easily look up the namespace and find the class in Reflector.
Reflector in the real World
I have a tool called West Wind Web Service Proxy Generator, which is a tool that creates Web Service clients from a WSDL document for Visual FoxPro applications. This tool works by looking at a WSDL document and create a .NET proxy class, and a FoxPro class that calls this .NET proxy class. In the process we end up with a 1-1 mapping of Web Service methods to FoxPro class methods.
Web Services can get very complex and most of the time these Web Services include a slew of support types that make up the various message objects that are used to pass parameters to and from the Web Service. For example, if you look at the Amazon SOAP Web Services you'll find that there are hundreds of support objects! But even simpler services end up at least with a handful of objects. Since .NET imports the WSDL from the Web and dynamically generates these types as part of the import there is no explicit documentation for them. A tool like Reflector is a life saver to figure out what the service makes available.
Here's an example:
In this case there's one service class (at the bottom) and about a dozen message classes that are used as part of the requests. Figuring out the full type names for each of these types is not exactly obvious and finding out each of the object's properties isn't either. It's tremendously helpful to be able to look at Reflector and to look at the method I'm calling, following it to the message type that needs to be sent to get the object name and then be able to access each of the properties of each of these objects.
It's invaluable.
Reflect on this!
Ok, so Reflector is very cool, but Reflector has kind of a checkered history and it's not the only game in town anymore. Reflector started out as an open source tool by Lutz Roeder who maintained it until version 4.0. Red Gate took over the product and although they promised to keep it free eventually started forcing people to upgrade to Version 7.0 with a nasty forced upgrade. I've been bitten by this as I've been linking Reflector as part of the West Wind Web Service Proxy Generator, so lots of people actually used Reflector in conjunction with this tool. When Reflector 7 came out Red Gate tried to force people to upgrade and when they didn't they actually removed the running copy of Reflector - it self-destructed and broke some of the links in my tool. Argh! I was pissed.
Red Gate recently reversed that last bit and now Version 6.8 can be used for free (if you can find a copy of V6) and it won't ask to upgrade or self destruct, but the catch is that you have to have an existing copy of Reflector 6 to upgrade first (Red Gate took down Reflector 6 from their servers).
All that said I think that Reflector is a worthwhile tool to spend $35 on even if all you do is COM Interop. The latest version has a number of improvements you know you're running a tool that is kept up to date with the latest features and versions of the .NET runtime.
You can grab an eval copy of Reflector from here:
http://www.reflector.net/
For what it's worth I use Reflector 7 and have upgraded because it's worth it and I have dependencies on it. I don't regret that, but I am pretty miffed at the way Red Gate handled this whole upgrade process.
Other Options
Because of the way that Red Gate blundered their marketing on the move to version 7.0 a lot of people were pissed off and a number of alternatives have sprung up, all of them free. Most of these provide similar functionality as Reflector does although the user interface might be slightly different (out of these JustDecompile is probably closest to Reflector).
- ILSpy
Open source tool that provides all the basic functionality of Reflector, but the UI is a bit more low level. Little harder to find things.
(requires .NET 4.0)
- JustDecompile
Telerik basically created a .NET Reflector clone using WPF. It's a nice and smooth application that provides the base features that .NET Reflector provides in a user interface that's very similar to Reflectors.
(requires .NET 4.0)
- DotPeek
JetBrains also has a tool that competes in this space. It's more techy and provides many additional .NET features that won't be so useful to Interop developers. But as the others it provides the necessary tools to figure out type names and method signatures reliably.
(requires .NET 4.0)
As you can see all the alternate tools require .NET 4.0 while Reflector 6 and 7 still run on Version 2.0 which is nice as .NET 2.0 is widely pre-loaded while 4.0 is still trying to reach critical mass. Otherwise each of these tools should do the trick of helping you in your development.
Monday, November 07, 2011, 1:21:11 AM
I'm starting this post with a disclaimer: This post doesn't have a full solution to the problem I was aiming to solve. However I'm posting it anyway to give some insights that might prove helpful to others and a partial solution that might address some limited scenarios that only need to generate display output to the console.
Here's the issue: I have a Visual FoxPro application that is a desktop application - Html Help Builder - but that also has command line options for a variety of tasks. When the app starts with no parameters it just runs the desktop app. But if you pass in a few specific parameters it will crunch away at some options and then exit. Along the way it would be nice to provide a little information, especially if something goes wrong. Specifically the process I'm working on is for Help File build automation with command line - you tell Html Help Builder to do a Help File compile and build - this process may take a little while and it be nice to provide some basic status information. For command line options that process is typically handled through Console output.
Alas, FoxPro - or for that matter any Windows application - can't output to Console by default. Windows apps start enter a message loop and exit back to the command prompt, and then continue running. Is it possible to write to the Console anyway? Sort of…
Getting output to the Console
After a bit of research I found a solution to create Console output via some C code in a DLL. Basically you can attach a console or create one for a process and then write to a console.
The code to do this is:
#include <windows.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#include <iostream>
#include <fstream>
BOOL WINAPI WriteToConsole(char *input)
{
BOOL bConsole = AttachConsole(ATTACH_PARENT_PROCESS) != FALSE;
if (bConsole)
{
int fd = 0;
long lStdOut;
lStdOut = (long)GetStdHandle( STD_OUTPUT_HANDLE); // STD_ERROR_HANDLE);
fd = _open_osfhandle(lStdOut, _O_TEXT);
if (fd > 0)
{
*stdout = *_fdopen(fd, "w");
setvbuf(stdout, NULL, _IONBF, 0 );
}
}
printf(input);
FreeConsole();
return TRUE;
}
I compiled this into wwIPStuff.dll (which includes a ton of other stuff used in our West Wind Client Tools) and I then created a small test program ConsoleOutput.prg in FoxPro that looks like this:
#include wconnect.h
DECLARE integer WriteToConsole IN wwipstuff.dll ;
string
? WriteToConsole(CRLF)
? WriteToConsole("Hello from FoxPro" + CRLF)
? WriteToConsole("More output" + CRLF)
? WriteToConsole("And still some More output" +CRLF)
? "Output from FoxPro Console"
WAIT window
I then add ConsoleOutput.prg to a project and compile this into an EXE:
BUILD EXE ConsoleOutput from ConsoleOutput
Good News, Bad News
To run this and get Console output to work you have to run the EXE from the Windows Command prompt. If you run from Explorer nothing is going to happen since there's no console active when an app is started from Explorer, the Run box or through code.
So open a command window and run the code. What you'll end up with is something like this:
It's clearly working - output *is* written into the Windows console.
But if you look closely, you'll notice that something funky is going on with the command prompt. Notice that there's an empty command prompt followed by console output, and then after the program is done executing the cursor remains in 'empty space' after the console output.
The command prompt is actually active, so you can type any Console commands and it will work - there's just no prompt.
Why is this happening? Unfortunately, this is how a Windows application is supposed to work. A Windows app like Visual FoxPro (WinMain() entry point) starts, loads up its Windows message loop and then is supposed to exit the console and return to the command prompt. The application runs, but it runs independently of the command prompt. A true Console application however (Main() entry point) runs synchronously until the main function completes at which point the application exits and returns to the command prompt.
This is even more evident if you try to redirect output to a file. If you do:
consoleoutput > consoleoutput.txt
you'll find that a 0 byte file consoleoutput.txt is created, but it's empty and no matter what you write to the console, the file will remain at 0 bytes, because the console redirection expires when the exe returns to the command prompt.
The bad news is that although I've now managed to write to the Windows Console, the output it generates is not all that useful. Yes I can see the output in the console window (even though it looks rather funky because of the writing *after* the prompt), but the output can't be captured using all the common Windows mechanisms for redirecting the output.
For example in Html Help Builder and my compilation output, this is useless because what I need is a mechanism to capture the result from the compilation process so it can be integrated into a build process.
No Native FoxPro Solution
Unfortunately I think this is a problem that can't be solved using FoxPro code because of the way Windows GUI apps work. The immediate exit is not something that I think we can get around especially since the console exit occurs even before any FoxPro runs. I was thinking that maybe with some of Windows internal events you'd be able to hook into the message queue at startup, but even that won't work because by the time even the first line of Fox code runs it's too late.
Workaround: Create a front end Console Application
I did end up with a solution to the problem however, although it's a pretty roundabout way to get there:
I ended up creating a .NET Console application that acts as a front end to the FoxPro app. The .NET app automates the Html Help Builder COM object and then echos output back to the Console. Granted this is a bit more work, especially since the FoxPro app already had a command line interface, but it's a workable solution.
Here's what the .NET Console App looks like:
using System;
using Westwind.wwHelp;
using System.IO;
namespace HelpBuilderConsole
{
class HelpBuilderConsole
{
static void Main(string[] args)
{
Console.WriteLine("West Wind Html Html Help Builder Console");
if (args == null || args.Length == 0)
{
Console.WriteLine("\r\n* Available commands:");
Console.WriteLine("RESET");
Console.WriteLine("REINDEX \t\t[hbp Project FilePath]");
Console.WriteLine("BUILDPROJECT \t\t[hbp Project FilePath]");
Console.WriteLine("ASSEMBLYIMPORT \t\t[AssenblyFilePath] [NewOrExistingprojectFile]");
OnExit();
return;
}
string command = args[0].ToUpper();
if (command == "BUILDPROJECT")
BuildProject(args);
else if (command == "ASSEMBLYIMPORT")
AssemblyImport(args);
else if (command == "RESET")
Reset(args);
else if (command == "REINDEX")
Reindex(args);
OnExit();
return;
}
static void OnExit()
{
#if DEBUG
Console.ReadLine();
#endif
}
static void BuildProject(string[] args)
{
if (args.Length < 2 || args[1] == null)
{
WriteError(3, "Invalid or missing Project Filename");
return;
}
string projFile = args[1];
if (!IsFile(projFile))
return;
wwHelp help = wwHelp.CreateInstance();
if (!help.Open(projFile))
{
WriteError(24, "Unable to open: " + projFile + ". " + help.ErrorMsg);
return;
}
Console.WriteLine("Building Html Html Help Builder Project: " + projFile);
WriteProgress("Generating HTML...");
help.GenerateHtml();
WriteProgress("Generating Index...");
help.GenerateIndex();
WriteProgress("Generating Toc...");
help.GenerateToc(0,true,null);
WriteProgress("Compiling Project...");
string result = help.CompileProject();
if (result == null || help.Error)
{
WriteError(24, "Error compiling project: " + help.ErrorMsg + "\r\n\r\n" + result);
return;
}
WriteSuccess("Project built.\r\n\r\n* Compiler Messages:\r\n" + result);
help.Release();
help = null;
}
}
... additional methods omitted
The wwHelp object used here is a wrapper around the Html Html Help Builder COM object. Html Help Builder includes a .NET API that provides the .NET wrapper around the COM object which makes short. With this code in place it's now easy to do:
HelpBuilderConsole.exe RESET
HelpBuilderConsole.exe BUILDPROJECT "c:\HelpBuilder Projects\RazorHosting\RazorHosting.hbp" > BuildOutput.txt
It's not as clean as having the main wwHelp.exe app handle these tasks, but at least the required functionality is in place. It works and gets the job done.
You can build your Console app in any language that can produce one - .NET is probably the easiest, but you can use C++ or Delphi or Java etc. Any environment that can produce a Console app will do.
Resources
Tuesday, October 25, 2011, 1:31:13 AM
When using the Web Control Framework in Web Connection one question comes up frequently:
How do I move off a page and on to the next page when I’ve completed my form input?
This questions stems from the WCF’s forms based metaphor where by default every page posts back to itself. So when you have an input form filled with fields, those fields are filled out by the user and when he or she presses the Save button the form data is posted right back to the same page that holds all the form data.
This ‘Postback’ mechanism is actually quite common in forms centric input pages because user input typically has to be validated and if there’s an error the page has to be redisplayed with all the user entered values, along with some error information that describes what needs to be fixed. In the Web Control Framework this is the default mode operandi.
If you look at a Web Control Framework page in the markup you see:
<form id="form1" runat="server">
Which when rendered generates:
<form id="form1" method="post">
Overriding the <form> Target
Notice that no target is specified here, which makes browser resubmit the page to the current page. You can easily override this behavior however and simply force the page to submit to a completely different URL with:
<form id="form1" runat="server" target="SaveCustomer.ttk">
Now the form will submit to a completely different Web Control framework page. For WCF applications though this doesn’t make a lot of sense because all the benefits of the WCF – named form controls, automatic control postback values etc. – are all tied to the original page the POST operation originated from. So in SaveCustomer.ttk to capture all the form variables posted would require manual Request.Form("txtName") type logic to get all the names.
Frankly if you go this route, it’s better to use a classic Web Connection templates or scripts to render your views rather than going through a WCF page. The only benefit WCF presents in this scenario is easier layout.
Making Postbacks work for you
WCF pages are easy to work with because they are effectively classes that encapsulate a single form’s operation. They isolate all behavior associated with the capture, valdation and saving of a form and facilitate that process by easily binding and unbinding data from the UI into underlying data structures.
The process normally is:
- Display data in Page OnLoad or OnPreRender
- Capture Data in a Button Click Event(s)
Here’s an overly simple example of a page that uses a business object and displays and edits a customer object:
**************************************************************
DEFINE CLASS Customer_Page as WWC_WEBPAGE OF WWC_WEBPAGE_FILE
**************************************************************
*** Your Implementation Page Class - put your code here
*** This class acts as base class to the generated page below
**************************************************************
oCustomer = null
oLookups = null
#IF .F.
LOCAL this as Customer_Page_WCSX OF Customer_Page.prg
#ENDIF
FUNCTION OnLoad()
this.oCustomer = CREATEOBJECT("ttCustomer")
this.oLookups = CREATEOBJECT("ttLookups")
lcId = Request.QUeryString("Id")
this.oLookups.GetStates("TStates")
this.txtState.DataSource = "TStates"
this.oLookups.GetCountries("Tcountries")
this.txtCountryId.DataSource = "TCountries"
IF !this.oCustomer.Load(VAL(lcID) )
this.oCustomer.New()
ENDIF
IF !THIS.IsPostBack
THIS.DataBind()
ENDIF
ENDFUNC
************************************************************************
* btnSubmit_Click
****************************************
*** Function:
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION btnSubmit_Click()
this.UnbindData()
IF this.BindingErrors.Count > 0
this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following:")
RETURN
ENDIF
IF !this.oCustomer.Validate()
this.AddValidationErrorsToBindingErrors(this.oCustomer.oValidationErrors)
this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following:")
RETURN
ENDIF
IF !this.oCustomer.Save()
THIS.ErrorDisplay.ShowError("Unable to save: " + this.oCustomer.cErrorMsg)
RETURN
ENDIF
this.ErrorDisplay.ShowMessage("Customer saved")
ENDFUNC
* btnSubmit_Click
ENDDEFINE
This works great and is pretty easy to understand.
But back to the original question now: How do we handle navigating off this page after a record has been saved?
In the example above we show error messages in the case of an error which is fine. If there are validation or other input errors we want to stay on the input page and display error messages (ShowError( BindingErrors.ToHtml() ) for example.
However when we’ve saved data most of the time we want to display a quick note that says “Customer Saved” (as I do in the sample above), but then we’re still stuck on the customer page which may or may not what you want.
One easy way to handle this is to redirect:
IF !this.oCustomer.Save()
THIS.ErrorDisplay.ShowError("Unable to save: " + this.oCustomer.cErrorMsg)
RETURN
ENDIF
this.ErrorDisplay.ShowMessage("Customer saved")
Response.Redirect("default.ttk")
But when you do this you don’t get to actually see the Customer Saved message. A Redirect immediately redirects without ever showing the original page and so no Customer Saved message.
Using the Refresh HTTP Header
What’s really needed in this example is to briefly display the confirmation message and THEN navigate off the page to the default page. You can use the Refresh HTTP header to accomplish this with the following code:
IF !this.oCustomer.Save()
THIS.ErrorDisplay.ShowError("Unable to save: " + this.oCustomer.cErrorMsg)
RETURN
ENDIF
this.ErrorDisplay.ShowMessage("Customer saved")
*** Redirect in 4 seconds
Response.AppendHeader("Refresh","4; url=default.ttk")
This solves the problem nicely! Now the page displays the Customer saved message for a few seconds and then after 4 seconds navigates off to the new URL specified.
This solves the confirmation display problem and still allows us to leave the confirmation display logic on the current page, rather than having to display a separate confirmation page.
Additionally this is also super useful if you need to capture cookies or new session assignments etc. On older browsers an HTTP 302/303 Location request, did not send additional HTTP header information to browsers so persistence token like Cookies were invariably lost. To be save you shouldn’t set Cookies (or new Sessions) when calling a Redirect as these cookies may get lost. Using the approach above of first redisplaying the page before redirecting ensures that that page is fully posted back and any cookies etc. get set properly.
Keep in mind that both Response.Redirect and the HTTP Refresh header allow only for GET operations: You can’t POST data to the target page. At best you can pass data on the query string to the new page.
To Postback or not to Postback
There’s been a lot of discussion around whether Postbacks to the same page are a good practice or not. In my opinion most of the time they are the right choice. Even in MVC style applications that use scripts/templates it’s not uncommon to see same page Postbacks manually handled. 90% of the time same page form postbacks simply make sense because you can share the same code for display and save operations – most of the time a lot of logic is shared between these operations.
In the few cases where Postbacks are confusing you can choose to override the target and send the POST data to a separate page. Or if you are completing a task that needs you to move on Response.Redirect lets you explicitly move on to another page.
Saturday, October 15, 2011, 12:00:00 AM
In Windows 7 (and Vista before it) it's become ever more important to run certain tasks under administrative privileges. With User Access Control in place there are a lot of things that applications can no longer do. While it's a good idea to follow Windows guidelines for file storage, registry access and general security requirements it's not uncommon for some applications to require you to go against those security settings.
User Access Control and 'Administrator' Accounts
Under Windows 7 and Vista new user accounts by default are created as Administrator accounts. However, if User Access Control (UAC) is enabled (which is it is by default), this Administrator account is more of a pseudo-Administrator account that doesn't have full rights at all. Instead if UAC is enabled and any task that requires administrative functionality is accessed the UAC prompt pops up to confirm the operation. Often you also see the UAC icon overlay over an icon that requires UAC overrides.
The above is annoying at times, but at least it lets you know when you need to raise your permissions to allow an administrative feature.
Unfortunately Windows isn't smart enough to detect all operations that require raised elevations. In our own applications in particular if you try to access say the installation folder that operation will either silently fail without UAC prompts, or - almost as bad - use some of Windows' redirection features to write data out in different locations.
The bottom line is if you're running with UAC on, you are not a real Administrator and a number of operations might fail.
One option to run as a true administrator is to turn UAC off completely. As soon as you do your account becomes a full Admin account with full rights to the machine.
How UAC affects you in Code
If you're building applications, it's actually vitally important that you either know about or take into considerations the account restrictions of UAC based access. You can't write data into installation directories, no HKLM registry write access, and a host of other things.
This can be especially problematic for older applications - it used to be quite common to host data files in the application's install folder for example. Even if you move that data out to a user specific folder like MyDocuments there may still be other tasks that don't fit that mold. For example, several of my apps have application updaters that check for new versions online and update the running application. That simply will not work under UAC.
Checking for Administrator
Ultimately in some situations it's very useful for your application to check whether it's running under a true Admin account. Luckily this is actually easily done. Here's some FoxPro code using a Windows API call:
************************************************************************
* wwUtils :: IsAdmin
****************************************
*** Function: Determines whether user is an admin user by probing
*** access to HKLM registry key
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION IsAdmin()
LOCAL loAPI, lcVal
DECLARE INTEGER IsUserAnAdmin IN Shell32
TRY
lnResult = IsUserAnAdmin()
CATCH
*** OLD OLD Version of Windows assume .T.
lnResult = 1
ENDTRY
IF lnResult = 0
RETURN .F.
ENDIF
RETURN .T.
ENDFUNC
* wwUtils :: IsAdmin
With this function in place you can now check before tasks that require admin access and pop up a message if the user is not set up as an administrator.
For example, in Html Help Builder when I attempt to do a code update from the Web site I check and if the user is not an admin pop up a detailed dialog box:
*** Admin Prompt
IF !IsAdmin()
TEXT TO RunAsAdminNote TEXTMERGE NOSHOW
<body style="font-family:verdana;font-size:10pt;top-margin:0" scroll="no">
<table><tr><td>
<img src="<< SYS(5)+CURDIR() >>bmp\images\alertIcon.gif" align="left" hspace="5">
<td><td align="center" valign="center">
<b style="font-size:12pt;color:maroon">Administrative Permissions Required</b>
</td><tr></table>
<hr style="color:darkblue;height:1pt;">
<p>
In order to download and install this Help Builder update you need to be
logged in as an Administrator.
<p>
You are currently <b>not logged</b> in as an Administrator.
<p>
To perform the Help Builder update, please exit the application
and restart it by using the Run As option and choosing a user
account that has Administrative priviliges from the
shortcut menu or from Windows Explorer (wwhelp.exe).
<p>
<b>What would you like to do?</b>
<ul><li> <b>Exit</b> Help Builder and show Help Builder install directory
<li> Just <b>Return</b> back to Help Builder
<li> Go ahead and <b>Try anyway</b> to update
<li> Get more information in the <b>Help</b> file
</ul>
</body>
ENDTEXT
lcResult = MessageDisplay(RunAsAdminNote,"Help Builder Update",;
"Exit,Return,Try anyway,Help",450,420,VARTYPE(goHelp) = "U")
DO CASE
CASE lcResult = 2 OR lcResult < 1
RETURN
CASE lcResult = 1
*** Update the version check date so it pops back up
loConfig.dLastVersionCheck = DATE() - 60
loConfig.Save()
IF (VARTYPE(GoHelp) = "O")
GoUrl(GetAppStartPath(),"RUNAS")
ENDIF
QUIT
CASE lcResult = 4
HELP ID 875
RETURN
ENDCASE
ENDIF && IsAdmin
This lets the user know specifically what the problem is and optionally directs them to start Help Builder as an Administrator with the RunAs option off the start menu or from Explorer.
This is a nice and non-intrusive way to handle the limited rights issues. Users that are running with admin rights never see the dialog. Those that aren't see the dialog and are given detailed advice on how to work around the issue.
Now, the hard part is isolating the hopefully few parts of your application where this sort of thing is necessary. Alternately if your app always needs full admin rights you can pop this sort of thing up right on application startup and force users to run as administrators.
© Rick Strahl, West Wind Technologies, 2004 - 2012