Quantcast
Channel: Rick Strahl's FoxPro and Web Connection Weblog
Viewing all 133 articles
Browse latest View live

Launching FoxPro in a Project Folder

$
0
0

I work with a lot of different customers that use FoxPro to build applications, and it always amazes me when I see developers launching into their application by starting FoxPro and then explicitly navigating - via CD commands or even interactively - to the actual project folder for about a minute.

To me that seems a crazy proposition: When you launch FoxPro you should be able to consistently start in a known configured environment. When you launch via a generic FoxPro command and navigate to the project folder you may or may not get a pre-configured environment that is ready to run your application.

Project Configuration

I like to configure projects in such a way that I have a reliable way to start them:

  • In the known project folder location
  • With a clean start up environment configured
    • config.fwp - Base environment configuration
    • Startup.prg - For more complex stuff, launch from config.fpw via COMMAND=
  • Dependency paths added to the SET PATH setting
  • Common SET Variables set:
    • EXCLUSIVE OFF
    • DELETED ON
    • EXACT OFF
    • CENTURY ON
    • SAFETY OFF
    • STRICTDATE 0
    • REPROCESS 1 SECOND
    • DEBUG ON
    • DEVELOPMENT ON
    • RESOURCE ON

Here's what my Web Connection Development startup config.fpw looks like for example:

SCREEN=On
TITLE=West Wind Web Connection 7.32
EXCLUSIVE=OFF
DELETED=ON
EXACT=OFF
DEVELOPMENT=On
DEBUG=ON
SAFETY=OFF
CENTURY=ON
STRICTDATE=0
MEMOWIDTH=200
RESOURCE=ON
REPROCESS="1 second"
RESOURCE=.\FoxUser.Dbf
PATH=.\classes; .\;  .\tools;.\console;.\samples\wwipstuff_samples;.\samples\wwdemo;.\samples\wwdotnetbridge;
COMMAND=DO wcStart

Your default environment settings may be different, but the point is that they are defined clearly here in the startup config.fpw file.

All of this ensures that if I start FoxPro out of the project folder my FoxPro environment will look a very specific way and it's ready to start running my application, without having to do additional configuration or manual folder navigation that takes up valuable seconds development time.

If your environment setup is more complex - say you need to map drives and set up other things I highly recommend you create a Startup.prg file that can perform all of those tasks programmatically. In that file you can do anything from setting the environment values that I also set in config.fpw but you can also run complex operations like making API calls to mapping drive mappings, or make wwDotnetBridge calls to set other system related settings or get startup information from services on your network or the internet. Anything at all really...

If you are using a complex setup it's probably best you only set development environment related settings in config.fpw and let the rest be handled by Startup.prg. This is a simpler version that defers to Startup.prg

SCREEN=On
TITLE=My Great Application v1.1
DEVELOPMENT=On
DEBUG=ON
RESOURCE=ON
RESOURCE=.\FoxUser.Dbf
COMMAND=DO Startup.prg

The nice thing about a Startup.prg file is that you can also run it from within the environment to 'reset' everything.

Application Configuration is Separate

It's important you don't rely solely on external configuration of your environment for your production applications. It's one thing to have a development configuration that auto-configures your development setup, but it's quite another for your deployed application running for customers.

It's a good idea to have very clear environment configuration logic inside of your application. Whether you include something like Startup.prg directly into your application, or whether you create a separate explicit configuration (I typically have an appName_load.prg I call as part of my startup), you need to ensure your application can configure itself without any prerequisite environment requirements.

Nothing pisses customers off more than you saying: Oh you have to start the application out of this folder or you have to make sure that X is set.

Do whatever possible to make your application self-configurable even if launched in non-standard situations.

Launching your Development Project Environment

The key to making all of this work however is to ensure that you start with a clean environment in the project folder. This means avoid launching some generic FoxPro instance and then navigating to the project folder. For one, you can't take advantage of the config.fpw launch settings which is very useful and serves as a good default environment configuration record. But also if you CD into the project folder you may have other environment settings already running, paths configured, libraries loaded into memory that might interfere with your actual application.

This is why a clean start in the startup folder IMHO is so desirable - you'll know you have a clean environment and you can get quickly back to if for some reason the environment becomes 'corrupted' or heck simple if FoxPro crashes.

So how do you launch your App out of a specific folder? There are a few ways that I use:

  • Create a Desktop Shortcut
    • Place the shortcut on the desktop
    • Place a similar shortcut into the project folder (might be slightly different)
    • Web Connection automatically creates a Project Shortcut on the Desktop and in Project Folder
  • Use a PowerShell script to launch FoxPro generically in the project folder

Desktop Shortcut

Desktop Shortcuts are nice for local machines and developer setups because they are easy to launch and visually pleasing if you assign an appropriate icon to it. On the downside shortcuts require hard coded paths to the FoxPro installation, so when you manually create a shortcut you have to find the FoxPro Exe path to use. You'll have to do the same for a custom icon if you don't want the default FoxPro icon.

Here's what the Web Connection project shortcut looks like:

Target:
C:\programs\vfp9\vfp9.exe -c"D:\WCONNECT\config.fpw

Start in:
D:\WCONNECT\

Icon:
D:\WCONNECT\wconnect.ico

Notice that I explcitly provide the startup folder for the project, but also the explicit path to the project's config.fpw file. This ensures that the specified config.fpw is used and not the default file that might be configured in your FoxPro IDE settings. This one has bitten me on a number of occasions where my config.fwp settings were ignored when I launched out of the project folder, but didn't explicitly provide the -c configuration file override.

You can place a shortcut like this on the desktop or on the Windows Taskbar for quick access on busy projects, and also add it into your project folder.

For example, when Web Connection creates a new project it automatically creates a desktop and project folder shortcut for you. Alternately if you don't auto-generate a shortcut, just copy the desktop shortcut into the project folder.

Creating a Desktop ShortCut with FoxPro Code

As an aside you can create a desktop shortcut programmatically using the Wscript.Shell COM interface:

IF IsComObject("Wscript.Shell")
	*** Create a desktop shortcut
	loScript = create("wscript.Shell")

	lcPath = SYS(5)+CURDIR()

    IF    MessageBox("It's recommended that you use a desktop shortcut"+CHR(13)+;
              "to start Web Connection in order to configure" +CHR(13)+;
              "the development environment on startup."+CHR(13)+CHR(13)+;
              "Do you want to create the shortcut now?",32+4,"West Wind Web Connection") = 6
                  
       	lcDesktopPath = loScript.SpecialFolders("Desktop") 

		loSc = loScript.createShortCut(lcDesktopPath + "\Web Connection.lnk")
		loSC.Description = "West Wind Web Connection"
		loSC.IconLocation = lcPath + "wconnect.ico"
		loSC.WorkingDirectory = lcPath
		loSC.TargetPath = lcVFP
		loSC.Arguments = [ -c"] + lcPath + [config.fpw"]
		loSC.Save()
	ENDIF
	...
ENDIF	

Creating a Generic PowerShell Launch Script

As nice as a desktop short cut is, it's not portable. Shortcuts don't work with relative paths, and you pretty much have to hard code the path to both the FoxPro IDE and location of the startup folder and config.fpw. This means every time you move the project you have to adjust the shortcut. It's not a huge deal, but if you share your project in a source code repository, shortcuts likely won't work necessarily across machines - in fact you probably should exclude shortcuts from committing into the SCC.

To provide a more generic solution that can:

  • Find the FoxPro installation
  • Determine the project relative path
  • Build an execution command

you need something that can execute some code. Preferably something that can generically retrieve the location of the FoxPro installation.

You can accomplish this with a small PowerShell script. Here's a generic launcher that also gets created into a new Web Connection project:

###########################################################
# Project FoxPro Launcher
#########################
#
# This generic script loads FoxPro in the Deploy Folder and
# ensures that config.fpw is loaded to set the environment
#
# This file should live in any Project's Root Folder
###########################################################

# Force folder to the script's location
cd "$PSScriptRoot" 

# Get VFP Launch Command Line
$vfp = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Classes\WOW6432Node\CLSID\{002D2B10-C1FA-4193-B134-D86EAECC5250}\LocalServer32")."(Default)"
if ($vfp -eq $null)
{
   Write-Host "Visual FoxPro not found. Please register FoxPro with ``vfp9.exe /regserver``" -ForegroundColor Red
   exit
}

$vfp = $vfp.Replace(" /automation","")
$vfp

# Launch VFP from the .\Deploy folder
cd .\Deploy
$path = [System.IO.Path]::GetFullPath(".\Deploy")
$path

& $vfp -c"$path\config.fpw"

# Command line back to project root
cd ..\

Unlike the shortcut, this code discovers the FoxPro install location when it runs, and can use relative paths based on the script's startup path. In other words, it's totally portable and can be shared by multiple developers in a shared project regardless of whether their base folders are different or not.

The code reads out the FoxPro installation location out of the registry in case installed in a non-default location (my scenario) or if using a locale that might not be using the English Program Files (x32) location.

The registry holds the location of the registered FoxPro IDE runtime - a ClassId for the COM object that refers the VisualFoxPro.Application - and this code strips that out from the /automation command that COM uses to launch the FoxPro IDE.

In this case the rest of the code then is specific to the Web Connection installation which launches the FoxPro out of the .\Deploy folder rather than the project root where the .ps1 script file lives. This might vary for your application, but because you can use relative paths here, this can be made to work generically regardless of what your project root folder is.

Here's how you can run the script from Explorer in the project root folder (in this case a Web Connection project):

If you have a recent version of PowerShell Core installed you can use Run with PowerShell context menu option to run the command. There are also PowerShell installation configuration options that allow you to run .ps1 scripts on double click which is not the default (like .bat or .cmd - but hey, Windows security idiocy!) .

The advantage of the PowerShell script is three-fold:

  • It's fully self-contained
  • It's portable across machines as you can use relative paths
  • It's text and can be shared in Source Code Repositories

Summary

Having a clean startup development environment is crucially important, especially if you work with many projects side by side. It saves time, reduces mistakes and makes your launch configuring automatically repeatable. It's easier to maintain as you don't have to remember random startup and launch instructions especially if you step away from the project for a few months or years and then come back.

I work with many customers and it's not uncommon for me to have multiple projects open, running and working on all of them at the same time. With totally isolated development environments that are self-configuring this process is easy.

To those of you that are already doing this you're probably nodding your head and going Why are you telling me this? It's bloody obvious! But as I mentioned I see a lot of developers not doing this, and literally firing up either in one standard application, or even the generic Foxpro launch icon and manually navigating and then fiddling with configuration settings.

Even if you're working only on a single project, it's useful to have a simple launch sequence that gets you right into a runnable environment. Don't waste time and keystrokes on navigating to where you have to be or running commands to get the app ready - automate that process. If you're not you're also making it harder for on-boarding of new developers who have to follow the same inconsistent operations.

Do yourself a favor and make sure your environment is easily launchable and runnable, from the moment the FoxPro Command Window comes up...

this post created and published with the Markdown Monster Editor

Web Connection 7.35 released

$
0
0

Web Connection Logo

A new release of West Wind Web Connection is available. v7.35 is a relatively small maintenance release that features a few small bug fixes, a few small feature updates and a couple of fairly large improvements to REST Service servers and clients.

Download Web Connection

You can download the latest version from the Web Connection Web Site:

For a more detailed version of what's new and improved, read on...

Version 7.35

Let's take a look at the West Wind User Security Manager which is meant to provide a more full featured experience around HTML based authentication including things like password resets and recovery, validating email addresses, and basic user management.

This has all been working for years of course and nothing is changing with that.

Authentication in HTML based Web Applications

For regular HTML page Web applications using MVC or raw HTML output Web Connection has support for some basic Authentication functionality via the wwUserSecurity class, which is based on HTTP Cookies tied to a wwSession record, which in turn maps to a user record in wwUserSecurity. If your needs are simple, you can just use the user table and user access as is, or you can customize both the structure and the logic by subclassing wwUserSecurity to handle how users are authenticated.

This is great for HTML based Web applications that can use cookies: You have a login form, you sign in, get assigned a cookie and the cookie then follows you around in subsequent requests in the browser, and the server can grab the cookie and map it to a session and/or a User record.

If you want more features you can also look at the West Wind User Security Manager which provides additional features for user signup, email validation, password recovery, and user management.

REST Service Authentication

REST Services however have other requirements in order to do security. Typically services can't use Cookies because clients often aren't browsers, or even if they are they might be connecting across different domains that can't persist a cookie.

REST applications tend to use Bearer Tokens which is basically an HTTP Header in a sepecific format that looks like this:

Authorization: Bearer <token>

Bearer tokens are issued by some sort of authentication process which can either be something simple as another API call to Signin with username and password (or some other combination), or something more complicated like an oAuth service that provides the token externally.

In order to provide some base functionality for REST Service Authentication v7.35 introduces a couple of new wwRestProcess class methods that can be used to provide Bearer Token based Authentication:

  • InitTokenSession()
    This method is the core feature that ties a generated token to a Web Conenction wwSession object via its cSessionId parameter. InitTokenSession() either retrieves an existing session from a provided Bearer token, or if one isn't provided or matched provides an empty session. To create a new Token you can have a custom sign in method and call NewSession() to map your custom user/customer/etc. to a session with a session holding any additional data.

  • Authenticate()
    If you want basic mapping of the session to a user in a similar way to the way HTML authentication works with cookies you can use the Authenticate() method which serves a dual purpose for:

    • Validating a wwSession Token and Loading a User
    • Authenticating user credentials

You can think of InitTokenSession() as a low level implementation. If you simply want to generate a token and provide a simple mapping to a user id or other reference, then all you need is wwRestProcess::InitTokenSession().

If you want to map a user record that can work with wwUserSecurity then you can use wwRestProcess::Authenticate(), which uses the existing wwUserSecurity infrastructure to map tokens to users and provides both the wwSession and wwUserSecurity and user to your process methods.

The Authenticate() call builds ontop of the InitTokenSession() functionality so both are needed in order to use Authenticate() just like InitSession() is required for Authenticate in HTML wwProcess authentication.

Implementation: InitTokenSession()

You can use pure InitTokenSession() session based authentication if your needs are very simple and you simply need to validate a user, and then only need to know if the user is logged in or not. If you don't need access to a user record or anything more complex than a mapping ID that you can use to map to your own business objects, using this approach is easiest and most light weight as it uses only a Session object and table.

InitTokenSession() is used to initialize the wwSession instance and guarantees that a session is loaded.

  • If a Bearer Token is found and can be mapped to a session that session becomes available in THIS.oSession.

  • If there's no match an empty session that doesn't have any backing on disk yet is returned.

There are two steps to this process:

  • Call InitTokenSession() in OnProcessInit()
  • Create some a Signin method to create and return the Bearer Token to the client

The first step to use token handling via wwSession is to call InitTokenSession() in OnProcessInit() so it fires for every request:

FUNCTION OnProcessInit

Response.Encoding = "UTF8"
Request.lUtf8Encoding = .T.

...

*** Pick up existing token or create a new token
*** and set on the oSession object
THIS.InitTokenSession()

*** Define anonymous requests that don't need validation
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,"testpage","signin")

*** Fail if not authorized (ie. a new session has no or failed bearer token)
IF !llIgnoreLoginRequest AND this.oSession.lIsNewSession
   THIS.ErrorResponse("Access Denied. Please sign in first.","401 Unauthorized")
   RETURN .F.
ENDIF

RETURN .T.

After InitTokenSession() returns, the THIS.oSession (or Session inside of your Process methods) is available with either a matched session instance, or a new empty session.

You can check whether a session is valid by looking at THIS.oSession.lIsNewSession. If .T. the session is not mapped to an existing token, but a new empty session. If the session is empty you likely don't want to allow access to the application except to some specific requests that have to be anonymous - like the sigin for example since you can't require an authorized user, before you have chance to sign in. ??

The code above shows how exclude certain requests based on the page names.

If this simple validation fails, you can use THIS.ErrorResponse() to return an error message and status code, which is returned as a JSON object response with the specified status code. This method directly writes a JSON message into the Response output.

Signing in

Before you can have a valid Bearer Token to check, you of course need some mechanism to sign in. If you use pure Token authentication, rather than user security, you can use any logic to validate your user.

Here's what this might look like using an arbitrary business object:

*** A simple REST Process Method  POST Signin.tp  - { username: "email@test.com", password: "superSeekrit" }
FUNCTION Signin
LPARAMETER loCredentials

*** Load some business object that can authorize
loBus = CREATEOBJECT("cUser")

*** Use whatever custom Authorization you need to assign a token
IF !loBus.AuthorizeUser(loCredentials.UserName, loCredentials.Password)
   RETURN THIS.ErrorResponse(loBus.cErrorMsg,"401 Unauthorized")
ENDIF

*** Create a new Session and optionally assign a mapping user id
*** that links back to a user/customer record in the Application
lcToken = THIS.oSession.NewSession(loBus.oData.UserId)
THIS.oSession.SetSessionVar("tenant",loBus.oData.TenantId)
THIS.oSession.SetSessionVar("displayname",loBus.oData.dispName)
THIS.oSession.Save()  && Must explicitly save to Db


*** Return the token and expiration (or whatever you choose)
loToken = CREATEOBJECT("EMPTY")
ADDPROPERTY(loToken,"token", lcToken)
ADDPROPERTY(loToken,"expires", DATETIME() + 3600 * 24)

RETURN loToken  
* Returns JSON: { token: "<token>", expires: "2023-10-23T07:00:00Z" }
ENDFUNC

The AuthorizeUser() call could be any application logic of your choice that returns true or false.

If a user is validated we can create a new Session, and provide a mapping user id (optional). At this point you can also write additional values into the Session object that you can later retrieve if needed.

For rich client JavaScript applications you might also want to return additional information like more user information (display name, initials, sub-ids etc. as well as user settings that are applied to the user interface). You control what the return object looks like, so return whatever you need in addition to the token.

For example:

{
    "token": "dre143adxq435o0",
    "expires": "2023-10-23T07:00:00Z",
    "user": {
        "displayName": "jan doe",
        "initials": "jd",
        "employeeId": "4rwesi4s22om2"
    },
    "settings": {
        "displayTimeout": 8000,
        "useInjuryPrompt": false,
        "useUserManager": false
    }
}

REST Request Walk Through

Here's what all that looks like in a series of REST requests using West Wind WebSurge (but you can use any other tool like Postman):

Signing in to retrieve a Token

Sign In For Token

Once you have a valid token you can add it to a request in the Authorization header (in WebSurge you can add an empty header (Authorization Bearer ) and it will auto-fill from the saved token).

Accessing a request with a valid Bearer Token

Access Request With Valid Token

Accessing a request with invalid or missing Bearer Token

Unauthorized Signin

The above provides the core functionality of mapping a Bearer Token to a session record which works for simple authentication where you don't need to track a user, only if a user is signed in. If you want to also map to a wwUserSecurity user record so you can access user information that is mapped between wwSession and a user record, then you can add Authenticate().

Once we add Authenticate() to the above flow, the HTTP operations will look identical. The only difference is how the token, session and user are mapped on the server.

Adding User Authentication()

The basis for Authenticate() is pretty similar to what I showed above. The difference is rather than checking for validity of a token, and creating a new token directly on the wwSession instance, you defer that to the Authenticate() method.

Note: In order to use Authenticate(), InitTokenSession() is still required and we recommend you call it just as in the code above in OnProcessInit() as you can customize the behavior via its parameters. If you don't call it explicitly, it will be called in Authenticate() but always without any parameters.

The Authenticate() method has three different modes:

  • Authenticate() - Validate a token from wwSession
    This methods looks at wwSession and looks for a non-empty session and if it find one uses the Authentication Session value to try and map the user id to a wwUserSecurity user record. If there is no session or the session's user entry can't be mapped a 401 result is returned.

  • Authenticate(lcUsername, lcPassword) - Authorize a user
    This version of the method can be used to validate a user agains the wwUserSecurity and it's underlying data store by validing a username and password. Username can be anything but typically will be an email address.

  • Authenticate("LOGOUT") - Clear Session and Token Association
    This operation logs out a user, removes the token to effectively sign out the user. Subsequent requests to use the existing token will then no longer succeed.

To implement then is again a 3 step process:

Initialize and Authenticate Token

This code is very similar to what I showed earlier for InitSessionToken() but this time we use Authenticate() to validate whether the token is valid and maps to a user:

FUNCTION OnProcessInit

...

*** IMPORTANT: InitTokenSession is required to pick up the Bearer token
***            and load or create a new session
THIS.InitTokenSession()

*** Check for pages that should bypass auth - signin always (not signout though!)
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,"testage","signin")

IF !llIgnoreLoginRequest
   *** Check for Authentication here based on the token (note no parameters)
   IF !this.Authenticate()   
	   THIS.ErrorResponse("Access Denied. Please sign in first.","401 Unauthorized")
	   RETURN .F. && Response is handled
   ENDIF
ENDIF

RETURN .T.
ENDFUNC

If a user is signed in, THIS.oSession, THIS.oUserSecurity and THIS.oUser properties will all be set and ready to use.

Signing in

Next the sign in process again is very similar except this time we can use the Authenticate() method with user name and password to parameters to directly map to a user record:

FUNCTION SignIn(loCredentials)
LOCAL loToken, llError, lcErrorMsg, lcToken, ltExpires

lcErrorMsg = ""
lcToken = ""

*** Sign in: If successful sets oUserSecurity, lIsAuthenticated, cAuthenticatedUser etc.
IF this.Authenticate(loCredentials.Username, loCredentials.Password)
	lcToken = Session.cSessionId 
	ltExpires = Session.oData.FirstOn + Session.nSessionTimeout
	
	*** Add any custom values you might need to store and retrieve
	Session.SetSessionVar("tenant","TENANT_ID")
	
	Session.Save() && Explicit save: Session on REST Services don't save by default		
ELSE
   RETURN THIS.ErrorResponse(lcErrorMsg,"401 Not Authorized")
ENDIF


*** Response
loToken = CREATEOBJECT("EMPTY")
ADDPROPERTY(loToken,"token", lcToken)
ADDPROPERTY(loToken,"expires", ltExpires)

** Add other client cachable user data here as properties if you need

RETURN loToken
ENDFUNC

This is very similar to the previous code but a little simpler as Authenticate() abstracts away both the actual user authorization as well as creating the new session and storing the standard session variables.

Accessing Authentication Properties

Once this call completes successfully or when a Bearer token is successfully validated, all the wwprocess authentication properties are available and you can access them in your REST service Process methods:

FUNCTION RestMethod(lvParm)

*** This should not be necessary if you called `Authenticate()`
*** and filtered out non-authenticated requests
IF !THIS.lIsAuthenticated
    RETURN THIS.ErrorREsponse("Access denied: Make sure you are logged in.", "401 Unauthorized")
ENDIF

*** Capture common Auth Properties you can access
lcUsername = THIS.cAuthenticatedUser
lcDisplayName = THIS.AuthenticatedName
loUser = THIS.oUser   && wwUserSecurity user instance

...

RETURN loResult  
ENDFUNC

Signing out

The final piece is signing out which requires that you call a specific process method with a Bearer token (ie. authenticated) for the user to log out.

Sign out is pretty simple with authentication:

FUNCTION Signout()
THIS.Authenticate("LOGOUT")
JsonService.IsRawResponse = .T.
Response.Write( [{ message: "Successfully logged out." }])   
ENDFUNC

REST Service Authentication Summary

As you can see Auth is always a bit more involved than it might seem at a glance, but the process of implementing it in a REST service got a lot easier by reusing functionality in the wwSession class, and optionally mapping Bearer tokens to wwUserSecurity users.

wwDotnetBridge Improvements

One of the most used features these days in West Wind Tools is the wwDotnetBridge library which provides an interfact to call .NET code from FoxPro. v7.35 has a few improvements in this regard:

Better support for Task Exception Handling

Modern .NET relies heavily on asynchronous code via Tasks which is .NET's version of Promises, or in even more abstract terms function as an object. Tasks wrap an action - a block of code - that is asynchronously executed and in .NET can be awaited for completion. While the calling block is waiting, any following code can continue. Tasks are continuations of code and essentially provide a mechanism for client code to wait for code while something else is running.

There's no direct translation for tasks into FoxPro code. The closest thing we have in FoxPro are events or function callbacks. wwDotnetBridge has a few methods that deal with Tasks:

The first two methods basically take any .NET method and calls it asynchronously. The last one calls a method that returns a .NET Task or Task<T> result. All of these methods work by accepting an object parameter, where the object passed should have OnSuccess() and OnError() handler methods that are called when the async call completes.

v7.35 has a few improvements in these 3 methods in how they are handled when exceptions occur. Specifically prior to this release Task based exceptions were not reporting the correct error stack. In the new version the Task exception is properly handled and any exception now correctly returns the error message that occurs inside of the actual Task operation, not the wrapper method as before.

Likewise the other 2 methods also use special exception handlers that can deal with thread based exceptions better, ensuring exceptions are always marshalled back to the calling FoxPro UI thread.

Json Serializer Improvements

The wwJsonSerializer class is used heavily for REST services, but also for other application level JSON conversion tasks. There are a few improvements:

wwJsonServiceClient and wwJsonService in their own PRG file

We've moved these classes into its own dedicated PRG file from wwJsonSerializer to reduce dependencies of the wwJsonSerializer PRG file (ie. do wwJsonSerializer). Doing so removes a number of large dependencies if you don't use wwJsonServiceClient to make it leaner to integrate the serializer into other applications and make the code more easily discoverable.
** Remember if you use this class you'll have to add the SET PROCEDURE TO (or DO wwJsonServiceClient) explicitly now.

Optionally capture Request and Response Data in wwJsonServiceClient

To improve debuggability there's now a new lSaveRequestData flag on the wwJsonServiceClient class which when enabled captures both any data sent to the server, and the response returned back from the server in cRequestData and cResponseData properties.

Updated to latest Newtonsoft.Json Library

It's been a while since the last update of the .NET JSON library used to handle deserialization, and we've now updated to the latest version which is 13.0.1. This release has been out for some time so it's stable and in wide use.

Updating this library is somewhat important in order to ensure that the library is in sync with other third party libraries that use different versions. By having the latest version we can force rolling forward to our version via assembly redirects. In many cases using the latest version can reduce the need to use Assembly redirects - although that does depend on what other versions third parties might be using.

Summary

As always, not a lot of new stuff, but if you dig under the hood you can certainly find improvements. If you're using REST services you are likely to be able to take advantage of the new Authentication features as they greatly simplify adding it to your own apps. By having this mechanism, the help topics demonstrate the basic workflow needed to authenticate users, even if you end up rolling your own version.

In any case, have it at it and as always, please report any issues you run into on the message board in the Web Connection forum.

Aloha.

wwDotnetBridge and Loading Native Dependencies for .NET Assemblies

$
0
0

Loading Banner

When using wwDotnetBridge to access .NET components you may run into .NET components that have native binary dependencies. When I say 'native' here I mean, non-.NET Win32/C++ dependencies that are generally not following the same assembly loading rules as the .NET host.

An Example: NHunspell, Spellchecking and Word Suggestions

Just for context, this is the NHunspell Spell checking component I highlighted in a Southwest FoxSession all the way back in 2016. This library is based on the popular HUnspell native library, for which NHUnspell is basically a .NET wrapper. The .NET wrapper just makes the appropriate interop calls to native C++ library.

I created a FoxPro class around the .NET component using wwDotnetBridge:

*************************************************************
DEFINE CLASS HunspellChecker AS Custom
*************************************************************
*: Author: Rick Strahl
*:         (c) West Wind Technologies, 2015
*:Contact: http://www.west-wind.com
*:Created: 08/07/15
*************************************************************
oBridge = null
oSpell = null
cLanguage = "en_US"
cDictionaryFolder = "" && root

************************************************************************
*  init
****************************************
FUNCTION init(lcLang, lcDictFolder)

IF EMPTY(lcLang)
   lcLang = this.cLanguage
ENDIF
IF EMPTY(lcDictFolder)
   lcDictFolder = this.cDictionaryFolder
ENDIF
   
this.oBridge = GetwwDotnetBridge()
IF ISNULL(this.oBridge)
      ERROR "Failed to load HUnspell: " + this.oBridge.cErrorMsg
ENDIF

* ? this.oBridge.GetDotnetVersion()

IF !this.oBridge.LoadAssembly("NHunspell.dll")
  ERROR "Failed to load HUnspell: " + this.oBridge.cErrorMsg
ENDIF

IF !EMPTY(lcDictFolder)
	lcDictFolder = ADDBS(lcDictFolder)
ELSE
    lcDictFolder = ""
ENDIF

this.oSpell = this.oBridge.CreateInstance("NHunspell.Hunspell",;
                                  lcDictFolder + lcLang + ".aff",;
                                  lcDictFolder + lcLang + ".dic")
                                  
IF VARTYPE(this.oSpell) # "O"
   ERROR "Failed to load HUnspell: " + this.oBridge.cErrorMsg
ENDIF  
                                  
IF FILE(lcDictFolder + lcLang + "_custom.txt")
  lcFile = FILETOSTR(lcDictFolder + lcLang + "_custom.txt")
  lcFile = STRTRAN(lcFile,CHR(13) + CHR(10),CHR(10))
  lcFile = STRTRAN(lcFile,CHR(13),CHR(10))
  LOCAL ARRAY laLines[1]
  LOCAL lnX, lnLine
  lnLines = ALINES(laLines,lcFile,1 + 4,CHR(10))
  FOR lnX = 1 TO lnLines
      this.oSpell.Add(laLines[lnx])            
  ENDFOR
ENDIF
                                  
IF ISNULL(this.oSpell)
  ERROR "Failed to load HUnspell: " + this.oBridge.cErrorMsg
ENDIF

ENDFUNC
*   init

************************************************************************
*  Spell
****************************************
***  Function: Checks to see if a word is a known word in the dictionary
************************************************************************
FUNCTION Spell(lcWord)

IF ISNULL(lcWord) OR EMPTY(lcWord) OR LEN(lcWord) = 1
   RETURN .T.
ENDIF

RETURN this.oSpell.Spell(lcWord)
ENDFUNC
*   Spell

************************************************************************
*  Suggest
****************************************
***  Function: Gives back a collection of word suggestions for 
***            the passed in word
************************************************************************
FUNCTION Suggest(lcWord)
LOCAL loWords, lnx

loCol = CREATEOBJECT("collection")
loWords = this.obridge.InvokeMethod(this.oSpell,"Suggest",lcWord)
? lowords
? this.oBridge.cErrorMsg

lnCount = this.oBridge.GetProperty(loWords,"Count")
? this.oBridge.cErrormsg
?  "Count: " + TRANSFORM(lnCount)


FOR lnX = 0 TO lnCount -1
    lcWord =  loWords.Item(lnX)
    loCol.Add( lcWord )
ENDFOR


RETURN loCol
ENDFUNC
*   Suggest


************************************************************************
*  AddWordToDictionary
****************************************
FUNCTION AddWordToDictionary(lcWord, lcLang)

lcFile = "editors\" + lcLang + "_custom.txt"
AppendToFile(lcWord + CHR(13) + CHR(10),lcFile)
this.oSpell.Add(lcWord)

ENDFUNC
*   AddWordToDictionary

************************************************************************
*  Destroy
****************************************
FUNCTION Destroy()

*** MUST dispose to release memory for spell checker
this.oSpell.Dispose()
this.oSpell = null

ENDFUNC
*   Destroy

ENDDEFINE
*EOC HunspellChecker

To use it you just instantiate the class and pass in a dictionary, then check for spelling or get suggestions:

loSpell = CREATEOBJECT("HunspellChecker","en_US",".\Editors")   
? loSpell.Spell("Testing")
? loSpell.Spell("Tesdting")   

lcWord = "aren'tt"
? "Suggest Testding"
loSug = loSpell.Suggest(lcWord)
? loSug.Count
FOR EACH lcWord in loSug
   ? lcWord
ENDFOR
loSpell = null

loSpell = CREATEOBJECT("HunspellChecker","de_DE",".\Editors")
? loSpell.Spell("Zahn")
? loSpell.Spell("Zähne") 
? loSpell.Spell("läuft") 
loSug = loSpell.Suggest("Zähjne")
FOR EACH lcWord in loSug
   ? lcWord
ENDFOR
      
? loSug.Count
loSpell = null

I use this library both in Markdown Monster (in .NET) and Html Help Builder (FoxPro) to provide on the fly spell checkiing inside of Markdown documents. The above methods provide both for the error highligghting as well as the suggestions that are popped up in the editor.

Spell Checking In Action

Surprisingly, this is very fast even accounting for interop between JavaScript and FoxPro and FoxPro to .NET!

This is just to give you some context of a library that has a native dependency (and because I know some of you would ask about the functionality ??)

Library and Dependencies: Watch Load Location

So NHunspell has a .NET assembly NHUnspell.dll and a native dependency for the underlying Win32 32 bit hunspellx86.dll (or hunspell.dll for the 64bit version).

So the two file dependencies for the .NET component are:

  • NHunspell.dll (.NET assembly)
  • HUnspellx86.dll (Win32 native dll)

Standard.NET Assembly Loading Behavior

Natively .NET dependencies resolve only out of the EXE's startup path or an optional PrivateBin path that can be assigned in app.config or through the hosting runtime instantiation.

For a FoxPro application that would mean 1 of 2 things:

  • Your EXE's startup folder when running the EXE
  • VFP9.exe's startup folder

wwDotnetBridge Assembly Loading Behavior

wwDotnetBridge tries to be a little more helpful and basically tries to resolve assembly paths for you if you explicitly load assemblies via LoadAssembly():

loBridge = GetwwDotnetBridge()

*** Looks in current and FoxPro Path
loBridge.LoadAssembly("NHUnspell.dll")

If you specify a non-pathed DLL name, wwDotnetBridge does FULLPATH(lcAssembly) on it, which resolves to the current path or anything along the FoxPro path to eventually provide an absolute path to the DLL. .NET by default looks for other assemblies in both the startup path and the location an explicitly loaded assembly is loaded from. IOW, any child dependencies automatically inherit the path of the parent for trying to find assemblies.

Alternately you can also specify an explicit path directly:

loBridge.LoadASsembly("c:\libraries\bin\NHUnspell.dll")

All this is great and predictable, but... it only applies to .NET assemblies, not any natively loaded assemblies.

Natively loaded Dependencies

If a .NET component has a native dependency as NHunspell has in the form of hunspellx86.dll that native DLL either has to be explicitly discovered and found by the .NET component, or it uses the default path resolution.

Default Native Path Resolution

The default path resolution for a native dependency is that it is loaded only out of the launching EXE's startup folder.

Custom Native Path Resolution

Some libraries use custom path resolution which set up their own folders and EXPLICITLY load out of that folder. For example, the popular LibGit2Sharp library uses a custom runtime/win-x86 folder where win-x86 is the platform you are using (x86 for FoxPro 32 bit).

The best way to see where things are expected is to create a .NET project and install the NuGet package into it, and then see exactly where files are placed. Then use the same 'offset' in your own project: If it dumps it into the root, use the same folder as your EXE, if there's a runtime folder duplicate that structure and put the DLL there.

NUnspell Loading

So again for NHunspell launched from FoxPro this means:

  • Your EXE's startup folder
  • VFP9.exe startup folder if you're running in the Fox IDE

This means when you distribute your application as an EXE always make sure that you ship native dependencies in the application startup folder even if you otherwise move additional binary dependencies into another folder. I for example, always move my dependent .NET DLLs into a seperate BinSupport folder so I can easily find all my dependencies in one place. Just make sure that you don't put any native dependencies into that folder too.

How this can bite you

I'm writing about this because this bit me hard today. I recently set up a new laptop and ended up reinstalling all of my applications. Today I looked at Help Builder and started up my dev version of it, and found that it kept crashing on the spell library with .oSpell not found.

Turns out the problem is that I reinstalled VFP 9 and of course did not remember to install the hunspellx86.dll in that folder so the spell checker can work inside of the Visual FoxPro IDE. Once I moved the DLL into the VFP9 install folder all is well.

To be clear though - my deployed application that runs the EXE still works because the installer copies the hunspellx86.dll into the install directory - no issue there, but the VFP install is tricky because you're not going to see anything wrong until you run your specialized application weeks after installation. You the

Summary

The jist of this post is that when you use a library with native dependencies make sure you understand the native libraries have different load behavior than the .NET libraries, with the default behavior being that it will load out of the startup EXE's startup folder.

West Wind Client Tools 8.0 Release Notes

$
0
0

We've released v8.0 of the West Wind Client Tools. Although this is a major version update, this is not a huge release although there are a few noteworthy changes in the libraries that may require attention when upgrading. It's been 6+ years since the last release and there have been many improvements since so this release can be thought of as a version rollup release more than anything.

There are a few new features in this update however:

  • Refactored FTP Support for FTP, FTPS (FTP over TLS) and SFTP
  • New ZipFolder() functionality
  • Many improvements to wwDotnetBridge
  • Many functional and performance improvements in wwJsonSerializer

Upgrading

This is a major version upgrade, so this is a paid upgrade.

You can upgrade in the store:

If you purchased v7 on or after June 1st, 2023 (a year ago) you can upgrade for free until the end of the year, otherwise an upgrade is required for the new version (details on the upgrade link).

Refactored FTP Support

The FTP support in Web Connection and the Client Tools has been pretty creaky for years. The original FTP support was built ontop of the built-in Windows WinINET services. Unfortunately those services do not support secure FTP communication so we've always lacked support for FTPS (FTP over TLS). Some years ago I added the wwSFtp class to provide support SFtp (FTP over SSH) which mitigated some of the deficiencies, but FTPS tends to be pretty common as some popular servers like Filezilla Server use FTPS.

Long story short, in order to support FTPS I added a new wwFtpClient class that supports both plain FTP and FTPS in a more reliable manner. This new implementation is built ontop of a popular .NET FTP library that is more accessible, considerably faster, provides for logging and provides much better error handling in case of failures. The new wwFtpClient is considerably simpler than the old wwFTP class as it does away with all the WinINET related baggage. As such the model for wwFtpClient is simply:

  • Connect()
  • Run Ftp Commands
  • Close()

and those are the only interfaces supported. You can:

  • Download
  • Upload
  • DeleteFile
  • MoveFile
  • ListFiles
  • ChangeDirectory
  • CreateDirectory
  • RemoveDirectory
  • ExecuteCommand

The methods are simple and easier to use.

Along the same vein I've also replaced the wwSFTP class with wwSFtpClient which uses the exact same interface as wwFtpClient so both classes can be used nearly interchangeably. There some small differences in how connections can be assigned but otherwise both classes operate identically - the old classes were similar but not identical. The wwSFtpClient class uses the same SSH.NET .NET library as before, although it's been rev'd to the latest version.

The old wwFtp and wwSFtp classes are still available in the OldFiles folder and they continue to work, but the recommendation is to update to the new classes if possible as they are easier to use and more reliable with more supported types of connections (for wwFtpClient).

ZipFolder() and UnzipFolder()

ZipFolder() is a new library function in wwAPI that provides zipping functionality from a folder using the built in Windows zip services, meaning there are no external dependencies on additional libraries. These functions use .NET to handle the Zipping interface, which removes the dependency on the old DynaZip dlls.

The old ZipFiles() and UnZipFiles() functions are still available, but they continue to require the dzip/dunzip/zlib1 dlls.

wwDotnet Bridge Improvements

The last few release of wwDotnetBridge have seen a number of improvements on how result values are passed back to .NET fixing up more types so that they work in .NET. There has been a lot of work around Collection access related to the ComArray class that is returned for lists and collections. The new AddItem() method makes it easy to add items to a collection effectively and AddDictionaryItem() makes it easy to added to key and value collections. There are also some improvements on how ComArray .NET instances are managed and can be accessed that results in additional use cases that did not previously work in some scenarios.

Additionally there's been some work to clean up how exceptions are handled and returned in error messages which should result in cleaner error messages without COM artifacts gunking up the message text. We've also fixed exception handling for Task Async operations - exceptions are now returned from failed task operations, rather than the generic failure that was returned prior.

JSON and REST Service Calls

The wwJsonSerializer is probably one of the more popular Client Tools components and it's getting a few new features in this release. There have been many tweaks in the last couple releases. One has been to optimize EMPTY object serialization by skipping the exclusion list which is meant to prevent FoxPro base properties from rendering which can improve performance significantly on large lists. There are now options for how dates are handled either as local or UTC dates based on the incoming data's date formatting.

The JsonSerivceClient gets the ability to capture request and response data optionally which can be useful for debugging or logging/auditing.

Breaking Changes? No Except for FTP

Although this is a major version update, there are no breaking changes other than the new FTP classes. And for those you can continue to use the older wwFtp and wwSFtp classes if necessary.

You will have to make sure to update your DLL dependencies however, so make sure you update:

  • wwipstuff.dll
  • wwDotnetBridge.dll
  • Newtonsoft.Json.dll
  • FluentFtp.dll (for wwFtpClient)
  • Renci.SshNet.dll (for wwSFtpClient)

+++ Rick ---

C# Version Formatting

$
0
0

How many times have I sat down trying to get versions formatted correctly for display? Inside of an application to end user? Too many!

Sure, it seems easy enough - you can use version.ToString() or specific format stringlike $"{v.Major}.{v.Minor}.{Build}. That works, but you often don't want the .0 at the end. Trimming . and 0 also can bite you on a 2.0 release. So there are a few little gotchas, and I've been here one too many times...

Native Version Display

For the simple things there are many ways to display version information natively:

ToString()

var ver = new Version("8.1.2.3");
version.ToString();   // 8.1.2.3

var ver = new Version("8.0.0.0");
version.ToString();   // 8.0.0.0

Not much control there - you always get back the entire string.

##AD##

There's also an overload with an integer value that returns only n number of the components. n is 1 through 4 which refers to major, minor, build, revision respectively:

var ver = new Version("8.1.2.3");
version.ToString(2);   // 8.1

var ver = new Version("8.0.0.0");
version.ToString(3);   // 8.0.0

String Interpolation/Concatenation etc.

You can of course also build a string yourself which seems easy enough:

var ver = new Version("8.1.2.3");
string vs = $"{Major}.{Minor}.{Build}"   // 8.1.3

The Problem with Fixed Formats

The problem with the above is that in some cases you might not want to display all the information if for example the Build number is 0. Or maybe you want to display build and revision but only if those values aren't 0. For example, for a version zero version release0 you probably don't want to display 8.0.0.0 but rather use 8.0.

You might think that's simple enough too:

var ver = new Version("8.1.2.0");
string vs = $"{Major}.{Minor}.{Build}.{Revision}".TrimEnd('.','0');   // 8.1.2

... until you run into a problem when you have:

var ver = new Version("8.0.0.0");
string vs = $"{Major}.{Minor}.{Build}.{Revision}".TrimEnd('.','0');   // 8

Ooops!

Consistent Version Function

This isn't a critical requirement, but I have so many applications where I display version information to users that I finally decided to create a function that does this generically for me instead of spending a 20 minutes of screwing each time I run into this.

Here's what I ended up with:

public static class VersionExtensions
    {
        public static string FormatVersion(this Version version, int minTokens = 2, int maxTokens = 2)
        {
            if (minTokens < 1)
                minTokens = 1;
            if (minTokens > 4)
                minTokens = 4;
            if (maxTokens < minTokens)
                maxTokens = minTokens;
            if (maxTokens > 4)
                maxTokens = 4;

            var items = new int[] { version.Major, version.Minor, version.Build, version.Revision };
            items = items.Take(maxTokens).ToArray();

            var baseVersion = string.Empty;
            for (int i = 0; i < minTokens; i++)
            {
                baseVersion += "." + items[i];
            }

            var extendedVersion = string.Empty;
            for (int i = minTokens; i < maxTokens; i++)
            {
                extendedVersion += "." + items[i];
            }
            return baseVersion.TrimStart('.') + extendedVersion.TrimEnd('.', '0');
        }

        public static string FormatVersion(string version, int minTokens = 2, int maxTokens = 2)
        {
            var ver = new Version(version);
            return ver.FormatVersion(minTokens, maxTokens);
        }
    }
}

##AD##

Summary

Obviously not anything earth shattering here, but it might save you a few minutes trying to get a version string formatted. I'll probably be back here and maybe I'll even remember I added this as a utility helper to Westwind.Utilities

Resources

West Wind Web Connection 8.0 Release Notes

$
0
0

Web Connection Logo

It's been a bit and Web Connection is turning 8.0! After six years of incremental improvements it's time to rev Web Connection to its next major version.

Although this is a major version release that has a number of significant updates, there is only one specialized feature (FTP) affected with breaking changes, so if you're upgrading from a v7 version, upgrades should be quick and easy like most other minor version updates.

For the last few years, the focus of Web Connection has been on continuous, small incremental feature enhancements and improvements around the development and administration process. So rather than huge, disruptive major releases updates, there have been gradual updates that come integrated one small piece at a time to avoid the big version shocks. This version rollover release has a bit more in the update department, but still follows the incremental improvement model and other than a single breaking change with the new FtpClient classes, this release has no breaking changes from recent v7 releases.

Before we get into what's new, here are the links for the latest release, purchase and upgrade:

Existing Runtime License Upgrades

Runtime licenses don't have specific upgrade SKUs, but can still be upgraded at 50% of the full price. For upgrade runtime license purchases, pick a full runtime license and then apply the Promo Code: RUNTIME_UPGRADE if you qualify. This only applies only to Runtime Updates not to version updates or new purchases. Use this code with a single item of the Runtime you wish to upgrade only.

Free Upgrade if purchased after July 1st, 2023

If you purchased Web Connection 7.x on or after July 1st, 2023 you can upgrade for free until the end of 2024. Use promo code: FREE_UPGRADE. Use this code on an order with a single item of the Web Connection Upgrade only.

Note: Upgrades are always verified and these promo codes apply only to the specific upgradable item. Please use these specialized Promo Codes only on orders that qualify based on the two descriptions above. If you use these codes with other types of upgrades or orders your order will be rejected. We reserve the right to refuse upgrades based on non-conforming orders.

What's new

Let's take a look what's new in this release:

More info on what's new in recent releases check out the What's New Page in the documentation.

COM Server Management Improvements

For deployed applications Web Connection should be run in COM mode, and COM mode includes an internal instance pool manager that makes it possible to effectively run FoxPro single threaded servers in a multi-threaded environment with simultaneous request handling. Getting the single threaded (or STA threaded really) FoxPro to behave in pure multi-threaded environment of .NET is a complex matter and involves a lot of trickery to make it work consistently and reliably.

In this release the COM Pool manager has seen a major refactoring:

  • Faster, parallelized Server Loading and Unloading
  • Servers are available as soon as they load
  • Reliable loading and unloading
  • No more double loading or unloading
  • All instance exes are released on unload (no more orphaned servers)
  • Improved error logging especially in detail mode

Web Connection has a long history of using COM Servers for production environments and while the technology and the implementation worked really well over the years all the way back to ISAPI, .NET, and now .NET Core, there have always been a few rough edges when it comes to server loading and unloading especially in very busy and high instance environments.

This release addresses these issue with a completely new pipeline for COM server loading and unloading that is reliable and - as a bonus - much quicker through parallelization of the load and unload processes.

You can get an idea of load/unload performance in this screen capture which demonstrates 5 server instances under heavy load from a West Wind WebSurge load test run, with the server pool constantly being loaded, unloaded, run as a single instance and the Application Pool being restarted:

Web Connection Server Loading and Unloading
Figure 1 - Web Connection COM Server Loading and Unloading Improvements

You can see that server loading is very fast, and if you look closely you can see instances immediately processing requests as the servers load, while the rest of the pool is still loading.

Servers are now loaded in parallel rather than sequentially which results in servers loading much quicker than previously. Additionally, servers are available immediately as soon as they enter the pool, while others are still loading. Previously the load process was blocked and sequential loading caused a potentially significant delay before processing could start. This doesn't matter much if you're running a few instances, but if you're running 10 or as many as 40 instances as one of our customers does, startup time can be a significant issue.

Additionally we fixed some related issues that in some cases caused double loading of server instances. Because the COM server load process first unloads before loading new instances, it was possible previously to end up in a scenario where instances were asking to unload while new instances where already loading. All of these issues have been addressed in the latest release with some creative thread management - ah, the joys of multi-threaded development ??

The changes have been made both the Web Connection .NET Module and the Web Connection .NET Core Web Server.

The Web Connection .NET Core Web Server

The Web Connection .NET Core Web Server was introduced with Web Connection 7.0 primarily as a tool to allow you develop locally without IIS. But it also to allows you distribute a local Web server with your own applications that let you effectively build and distribute local Web Applications that can run on a desktop machine. The .NET Core server middleware also supports running inside of IIS (if you want consistency between dev and production) and can even be used on non-Windows platforms like Linux either as a standalone Web server or a behind a reverse proxy server like NginX (but the FoxPro code still has to run on Windows).

Improved Web Connection Server Error Logging

In the process of updating the Web Connection server connectors we've also reviewed and updated the server logging that goes into the wcerrors.txt logs. We've cleaned up the error logging so that non-detail mode doesn't log anything but critical messages and errors - previously there were some ambiguous trace messages that often came up in discussion on the forums, but weren't actual errors. These have been removed, and you should now see a mostly blank wcerrors.txt file in normal operation, except if you start having problems with your servers.

Detail logging (LogDetail true in the configuration) logs a lot of error and non-error information into the log including request start and stop operations, request timings, application start and much more detailed error information on errors.

Detail mode now always shows the request id and thread information (if available) to more easily correlate requests in a busy error log.

Web Connection Error Logging
Figure 2 - Web Connection Detail Error Logging

You'll also notice that the actual request completion or call error is marked with a *** prefix so it's more easily visible in the noise. The *** entries are either a completed request or the actual request processing error message that occurred during the COM call.

New FTPClient Classes

This release has a completely new set of FTPClient classes that replace the old wwFtp and wwSFTP classes. The new version uses a .NET based interface instead of the legacy WinInet features that are somewhat limited in that they didn't support the FTPS (FTP over TLS) protocol which to be frank makes them useless in today's environment where secure connections are a requirement.

The new version relies on two .NET libraries:

  • FluentFtp (for FTP and FTPS)
  • SSH.NET (for SFTP)

We've been using SSH.NET previously for SFTP support, but FluentFtp integration is new, and it provides for the new FTPS support in Web Connection (and the West Wind Client Tools).

The new classes are:

The two classes have the exact same API surface except for connection information which is slightly different for SFTP which requires SSH keys or configuration files instead of username and password for standard FTP.

The new classes follow a similar interface to the old connection based wwFTP/wwFTPS classes, so if you used them with Connect()... FTP Operation... Close() operations the syntax will be identical and easy to upgrade. In most cases you should be able to simply change the class name - ie. change CREATEOBJECT("wwFtp") to CREATEOBJECT("wwFtpClient").

What's missing from the old wwFtp and wwsftp classes are the single method FTP operations for uploading and downloading. These were awkward to use with their long parameter lists anyway and the class based interface is cleaner to use anyway. The old wwFtp and wwSftp classes are still shipped in the \classes\OldFiles folder and can still be used - just copy them into your path and they'll work like before.

To demonstrate the new FtpClient functionality, here's an example that runs through most operations supported:

CLEAR
DO wwFtpClient
DO wwUtils   && for display purposes only

loFtp  = CREATEOBJECT("wwFtpClient")
loFtp.lUseTls = .T.
loFtp.cLogFile = "c:\temp\ftp.log" && verbose log - leave empty
loFtp.lIgnoreCertificateErrors = .F.   && self-signed cert not installed


*** cServer can be "someserver.com", "someserver.com:22", "123.213.222.111"
lcServer =  INPUTBOX("Server Domain/IP")
IF EMPTY(lcServer)
   RETURN
ENDIF

lcUsername = InputBox("User name")
IF EMPTY(lcUsername)
   RETURN
ENDIF

lcPassword = GetPassword("Password")
IF EMPTY(lcPassword)
	RETURN
ENDIF

*** Progress Events - class below
loFtp.oProgressEventObject = CREATEOBJECT("FtpClientProgressEvents")

loFtp.cServer = lcServer
loFtp.cUsername = lcUsername
loFtp.cPassword = lcPassword
*loFtp.nPort = 21 && only needed if custom port is required

IF !loFtp.Connect()
	? loFtp.cErrorMsg
	RETURN
ENDIF
? "Connected to " + lcServer	

loFtp.Exists("Tools/jsMinifier1.zip")


IF !loFtp.DownloadFile("Tools/jsMinifier.zip", "c:\temp\jsMinifier.zip")
	? loFtp.cErrorMsg
	RETURN
ENDIF	
? "Downloaded " + "Tools/jsMinifier.zip"

lcUploadFile = "Tools/jsMinifier" + SYS(2015) + ".zip"
IF !loFtp.UploadFile("c:\temp\jsMinifier.zip", lcUploadFile)
	? loFtp.cErrorMsg
	RETURN
ENDIF
? "Uploaded " + lcuploadFile

*** provide a folder name (no wildcards)
loCol = loFtp.ListFiles("/Tools")
IF ISNULL(locol)
   ? "Error: " + loFtp.cErrorMsg
   RETURN
ENDIF   
? TRANSFORM(loCol.Count ) + " matching file(s)"
? loFtp.cErrorMsg
FOR EACH loFile IN loCol FOXOBJECT
   IF ( AT("jsMinifier_",loFile.Name) = 1)
	   ? loFtp.oBridge.ToJson(loFile)  && for kicks print out as json
	   IF loFtp.DeleteFile(loFile.FullName)
	      ? "Deleted " + loFile.FullName
	   ENDIF
	   
   ENDIF
ENDFOR

loFiles = loFtp.ListFiles("/Tools")
FOR EACH loFile in loFiles
   ? loFile.Name + " " + TRANSFORM(loFile.LastWriteTime)
ENDFOR

* loFtp.Close()  && automatic when released

RETURN


DEFINE class FtpClientProgressEvents as Custom

FUNCTION OnFtpBufferUpdate(lnPercent, lnDownloadedBytes, lcRemotePath, lcMode)
  lcMsg = lcMode + ": " + TRANSFORM(lnPercent) + "% complete. " + lcRemotePath + " - " + TRANSFORM(lnDownloadedBytes) + " bytes"
  ? "*** " + lcMsg
ENDFUNC

ENDDEFINE 

New wwZipArchive Class

This release also has a new ZipArchive class that provides more control over zip functionality using modern, native .NET and built-in functionality that removes the old dependency on Dynazip libraries.

The new class provides the ability to add files to existing zip files and iterate and retrieve files individually.

CLEAR
DO wwZipArchive

loZip = CREATEOBJECT("wwZipArchive")
lcZipFile = "d:\temp\zipFiles.zip"

*** Zip up a folder with multiple wildcards
*** 
IF !loZip.ZipFiles(;
   lcZipFile,;
   "*.fpw,*.vc?,*.dll,*.h",;  
   CURDIR(),;
   .T., .T.)
   ? "Zipping Error: " + loZip.cErrorMsg
   RETURN
ENDIF   
? loZip.cErrorMsg
   
*** add a single file   
IF !loZip.AppendFiles(lcZipFile, "wwZipArchive.prg")
   ? "Error: " + loZip.cErrorMsg
   RETURN
ENDIF
   
*** Unzip into a folder   
IF !loZip.UnzipFolder(lcZipFile, "d:\temp\Unzipped1")
   ? "Unzip Error: " + loZip.cErrorMsg
   RETURN
ENDIF

*** Look at all files in the zip   
loEntries = loZip.GetZipEntries(lcZipFile)
IF ISNULL(loEntries)
   ? "No entries:  " + loZip.cErrorMsg
   RETURN 
ENDIF
? loEntries.Count

*** Iterate through the collection
FOR EACH loEntry IN loEntries FoxObject     
	? loEntry.Name + "  " + ;
	  loEntry.Fullname + " " + ;
	  TRANSFORM(loEntry.Length) + " - " + ;
	  TRANSFORM(loEntry.CompressedLength)
ENDFOR   

*** Unzip an individual entry and unzip it - first in this case
loZip.UnzipFile(lcZipFile, loEntries[1].FullName, "d:\temp\" + loEntries[1].Name)

These functions use wwDotnetBridge and built-in .NET framework features for zipping files. Note that one thing missing here is support for encrypted Zip files which is not supported by the .NET APIs.

The old ZipFiles() and UnzipFiles() in wwAPI.prg are still available as well, but you need to make sure you have the dzip.dll and dunzip.dll files available in your distribution.

REST Service Token Authentication Support

Unlike standard wwProcess classes, wwRestProcess does not work with standard session cookies and by default all session support is turned off. However, you can enable session support via Bearer Token authentication which reads a user supplied identity token from the Authorization HTTP header.

There are two mechanisms available:

  • InitTokenSession
    This method is the core feature that ties a generated token to a Web Connection wwSession object via its cSessionId parameter. InitTokenSession() either retrieves an existing session from a provided Bearer token, or if one isn't provided or matched provides an empty session. To create a new Token you can have a custom sign in method and call NewSession() to map your custom user/customer/etc. to a session with a session holding any additional data.

  • Authenticate()
    If you want basic mapping of the session to a user in a similar way to the way HTML authentication works with cookies you can use the Authenticate() method which serves a dual purpose for:

    • Validating a wwSession Token and Loading a User
    • Authenticating user credentials

InitTokenSession() is the low level function that checks for bearer tokens and maps them onto a wwSession object. It generates new tokens on every request but only stores them if you explicitly save them in a sign in request. To see if a user has a previous approved token you can check !Session.lIsNewSession. This is pretty low level but provides to core feature of token management and whether a user has a token that matches an existing token.

FUNCTION OnProcessInit
...

*** Pick up existing token or create a new token
*** and set on the oSession object
THIS.InitTokenSession()

*** Define anonymous requests that don't need validation
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,"testpage","signin")

*** Fail if no token and not a passthrough request
IF !llIgnoreLoginRequest AND this.oSession.lIsNewSession
   THIS.ErrorResponse("Access Denied. Please sign in first.","401 Unauthorized")
   RETURN .F.
ENDIF

RETURN .T.

Authenticate() maps on top of that functionality by taking a mapped token and mapping it to an UserSecurity object, providing all the familiar User Security features like the .oUser, lIsAuthenticated, cAuthenticatedUser etc. properties on the wwProcess class.

FUNCTION OnProcessInit
...

*** IMPORTANT: InitTokenSession is required to pick up the Bearer token
***            and load or create a new session
THIS.InitTokenSession()

*** Check for pages that should bypass auth - signin always (not signout though!)
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,"testage","signin")

IF !llIgnoreLoginRequest
   *** Check for Authentication here based on the token (note no parameters)
   IF !this.Authenticate()   
	   THIS.ErrorResponse("Access Denied. Please sign in first.","401 Unauthorized")
	   RETURN .F. && Response is handled
   ENDIF
ENDIF

*** One you're here you can now acccess these anywhere in your process code:
llIsLoggedin = this.lIsAuthenticated
lcUsername = this.cAuthenticatedUser
loUser = this.oUser

RETURN .T.

Choose the Authenticate() approach if you need to know who your users are explicitly. Use the InitTokenSession() if you only need to know that they are have signed in and are validated. Authenticate() tries to map the token to a user and there are several overloads of this method with various parameter signatures. You can also override these methods with custom behavior for mapping users to tokens.

Beyond those two approaches you still need to actually validate a user via some sort of sign in operation that authenticates a user and then creates the actual token. This can be another endpoint or it could be an oAuth operation or even a standard Web page.

The following uses a REST endpoint in an existing API (ie. part of the REST service):

FUNCTION Signin
LPARAMETER loCredentials

*** Load some business object (or plain DAL code) that can authorize a user
loBus = CREATEOBJECT("cUser")

*** Use whatever custom Authorization you need to assign a token
IF !loBus.AuthorizeUser(loCredentials.UserName, loCredentials.Password)
   RETURN THIS.ErrorResponse(loBus.cErrorMsg,"401 Unauthorized")
ENDIF

*** Create a new Session and optionally assign a mapping user id
*** that links back to a user/customer record in the Application
lcToken = THIS.oSession.NewSession(loBus.oData.UserId)
THIS.oSession.SetSessionVar("tenant",loBus.oData.TenantId)
THIS.oSession.SetSessionVar("displayname",loBus.oData.dispName)
THIS.oSession.Save()  && Must explicitly save to Db


*** Return the token and expiration (or whatever you choose)
loToken = CREATEOBJECT("EMPTY")
ADDPROPERTY(loToken,"token", lcToken)
ADDPROPERTY(loToken,"expires", DATETIME() + 3600 * 24)

RETURN loToken  
* Returns JSON: { token: "<token>", expires: "2023-10-23T07:00:00Z" }
ENDFUNC

The focus behind this code is to create a new token with Session.NewSession() and then saving it into the session table.

The token is then returned to the client, who will then use it to pass in the Bearer token Authorization headers with their REST client requests. Something akin to this in FoxPro code:

loHttp = CREATEOBJECT("wwHttp")
loHttp.AddHeader("Authorization","Bearer " + lcToken)
lcJson = loHttp.Get(lcUrl)

All of this is designed to make it easier to create REST services that can authenticate without having to re-build a bunch of infrastructure. Instead this stuff re-uses what Web Connection already provides and exposes it to the newer REST service infrastructure with a couple of relative simple constructs you can add to your REST service with a few lines of code.

wwRestProcess.lRawResponse Helper Property

Speaking of REST services here's a small, but frequently used feature: There's now a Process.lRawResponse property that can be set to to .t. to return a raw, non-JSON response from a REST method. That functionality was always available via the JsonService.IsRawResponse, but it's a bit easier to set it on the local class instance.

So you can do the following in a REST method now:

FUNCTION ReturnPdf()

THIS.lRawResponse = .T.
Response.ContentType = "application/pdf"

 lcFilename = THIS.ResolvePath("~/policy.pdf") 

*** Send from memory - string/blob
lcFile = FILETOSTR(lcFilename)
Response.BinaryWrite( lcFile )

*** OR: Send from file
*!* Response.TransmitFile(lcFilename,"application/pdf")

ENDFUNC

wwDotnetBridge Improvements

There are a number of small tweaks to wwDotnetBridge as well in this release:

  • wwDotnetBridge.GetPropertyRaw() and ComArray.ItemRaw()
    Overridden methods that allow for retrieval of property values in raw format that bypass the usual FoxPro fix-ups that ensure type safe values are returned to FoxPro. Useful in scenarios where the values are sometimes in ComValue or ComArray that can be accessed directly, or in scenarios where types have dual meaning (ie. char with raw number vs. string fix-up or Guid with raw binary vs. string fix-up).

  • ComArray.GetInstanceTypeName() and ComArray.GetItemTypename() helpers
    Added a couple of helpers to the ComArray class to provide type information about the Array instance and it's client types for debugging or testing purposes. This can be useful to determine whether the .Instance member can be accessed directly via FoxPro code (many .NET collections cannot and require intermediary operations provided by ComArray or wwDotnetBridge).

  • wwDotnetBridge::DisposeInstance() to explicitly release Object Dependencies
    This method explicitly release IDisposable object instances by calling .Dispose(). Since .Dispose() tends to be an overloaded virtual property you typically can't call it directly on a .NET reference instance, so this method helps making a direct call rather than calling InvokeMethod().

  • wwDotnetBridge: Improved support for Task Exception Handling
    When making calls to .NET async or Task methods, wwDotnetBridge now does a better job of handling exceptions and returning the result in the OnError() callback. More errors are handled and error messages should be more consistent with the actual error (rather than a generic error and an innerException).

Json and REST Service Improvements in recent versions

  • wwJsonSerializer Deserialization Performance Improvements
    Optimized the .NET parsing of the deserialized object graph for improved performance. Also fixed a few small issues that previously could result in naming conflicts that FoxPro couldn't deal with. Fixed a small issue with UTC dates when AssumeUtcDates (ie. passthrough as-is dates) is set.

  • wwJsonSerializer no longer uses PropertyExclusionList on EMPTY Object
    When serializing EMPTY objects, or by association cursors and collections which internally use EMPTY objects, the PropertyExclusionList is not applied to properties. The list is meant to keep FoxPro default properties from polluting the output JSON, but EMPTY objects do not have any base properties, so the list is not necessary. This allows for creating properties with reserved FoxPro property names like Comment, Name, Classname etc.

  • Fix: wwJsonSerializer::AssumeUtcDates Output still converting to Local
    Fixed issue that when this flag was set, it would not convert the inbound date from local to UTC but use the current date as UTC, but it would still convert the date back to local when deserializing. This change now leaves the deserialized date in the original UTC time, but returns it as a local FoxPro time (ie. the date is not adjusted for timezone) which was the original assumption of this flag. This was broken when we switched from FoxPro based parsing to .NET parsing using JSON.NET. This is a potentially breaking change if you used this obscure flag in your code.

  • wwJsonServiceClient CallService Url Fix up
    You can now use a site relative URL by specifying a cServiceBaseUrl before calling CallService() which allows you to use site relative paths for the URL. You can use Urls like /authenticate which makes it easier to switch between different host sites. If the URL does not start with http:// or http://, the cServiceBaseUrl is prepended to the URL to create a full URL. This is useful if you switch between different sites such as running against different servers for dev, staging and production servers.

  • wwJsonServiceClient: Optionally capture Request and Response Data You can now optionally capture all request and response data via the lSaveRequestData flag. If set any POSTed JSON data will be capture in cRequestData and any result data is capture in cResponseData both of which are useful for debugging.

  • wwJsonServiceClient is abstracted into its own PRG File
    wwJsonServiceClient now has migrated out of the wwJsonSerializer.prg file to its own wwJsonServiceClient.prg file. This is a minor breaking change - you'll need to make sure DO wwJsonServiceClient is called explicitly now to ensure the library is loaded along with all dependencies.

  • wwJsonServiceClient CallService Url Fix up
    You can now use a site relative URL by specifying a cServiceBaseUrl before calling CallService() which allows you to use site relative paths for the URL. You can use Urls like /authenticate which makes it easier to switch between different host sites. If the URL does not start with http:// or http://, the cServiceBaseUrl is prepended to the URL to create a full URL. This is useful if you switch between different sites such as running against different servers for dev, staging and production servers.

  • Fix: wwJsonService UTF-8 Encoding/Decoding
    Fixed inconsistencies in UTF-8 encoding by the service client. Now data sent is encoded and data received is decoded. Optional parameters allow disabling this auto en/decoding.

wwCache Improvements

wwCache is an old component in Web Connection that is internally used to cache certain bits of information in a local cursor. It's a great way to cache generated output or any string based value that you don't want to repeatedly regenerate or calculate out.

The class gains a few common method that were previously missing: Clear() that clears the cache and closes the underlying cache cursor to avoid excessive memo bloat and GetOrAddItem() that combines retrieving an existing value, or setting a new one into the cache in one step.

Note that in Web Connection the cache object is always available as Server.oCache:

PRIVATE pcToc

*** Retrieve a cached TOC or generate one
pcToc = Server.oCache.GetOrAddItem("Toc",GenerateToc(),3600)

*** pcToc can now be embedded into the template
Response.ExpandTemplate("~\toc.wcs")

Summary

Overall this major version release has no groundbreaking new features, but there are a number of significant and useful enhancements. I think the COM server features in particular are going to be very useful to those of you running busy sites on Web Connection.

As we go forward Web Connection will continue to do incremental updates of features and roll them into minor release updates, rather than providing big bang new versions with massive amount of changes that few will use due to feature overload ??.

As always with new releases, please, please report any issues you encounter on the message board.

Aloha,

+++ Rick ---

Article 0

wwDotnetBridge Revisited: An updated look at FoxPro .NET Interop

$
0
0

Bridge

by Rick Strahl
prepared for Southwest Fox, 2024

Session Example Code on GitHub
Session Slides
wwDotnetBridge Repo on GitHub
wwDotnetBridge Docs

.NET has proliferated as the dominant Windows development environment, both for application development using a variety of different Windows-specific platforms and the high-level interface chosen by Microsoft to expose Windows system functionality. .NET based APIs have mostly replaced COM as the high level Windows system interface that exposes Interop features besides native C++.

More importantly though, .NET has garnered a huge eco system of open source and commercial libraries and components that provide just about any kind of functionality and integration you can think of.

All of this is good news for FoxPro developers, as you can take advantage of most of that .NET functionality to extend your own FoxPro applications with rich functionality beyond FoxPro's native features using either basic COM interop (very limited) or more usefully with the open source wwDotnetBridge library.

In this very long White Paper, I'll introduce wwDotnetBridge and some of the things that you probably need to know about working with .NET before diving into 10 separate examples along with some background on each, that each demonstrate different features of wwDotnetBridge. Most of the size is in these examples as they provide some background and in some cases quite a bit of code and you can skip those that don't interest you.

.NET History: .NET Framework to .NET Core

.NET has been around since the early 2000's and in those nearly 25 years it has undergone a number of transformations. From its early days as a limited distributed runtime, to integration into Windows as a core Windows component, to the splitting off of .NET Core as a cross platform capable version of .NET, to full cross-platform support for .NET Core, to recent releases that provide nearly full compatibility with .NET Framework for .NET Core including of Windows specific platforms (ie. WinForms, WPF, WinUI).

The most significant change occurred in 2016, when .NET split off into the classic .NET Framework (the Windows native Runtime build into Windows) and .NET Core, which is a newly redesigned version of .NET that is fully cross-platform enabled and can run on Windows, Mac and Linux and that is optimized for performance and optimized resource usage. The new version has greatly expanded .NET's usefulness and developer reach with many new developers using the platform now.

This new version of .NET - although it had a rough initial start - is also mostly compatible with the classic .NET Framework and can for the most part run code on both frameworks interchangeably. .NET Core brought a ton of improvements to .NET in terms of performance and resource usage, as well as new server side frameworks (ASP.NET, Blazor, Maui etc.),and much simpler and universally available tooling that removed the requirement for developing applications exclusively on Windows using Visual Studio.

Today you can build applications for Windows, Mac or Linux, developing applications on these platforms using native editors either with integration tooling or command line tools that are freely available via the .NET SDK. The SDK includes all the compiler tools to build, run and publish .NET applications from the command line without any specific tooling requirements.

.NET and FoxPro

For FoxPro developers it's preferable to use components that use the old 'full framework' libraries when available even though .NET Core is the new and shiny new framework. The full .NET Framework (NetFX) is part of Windows and so it's always available - there's nothing else to install to run it and so it's the easiest integration path for FoxPro applications. For this reason I strongly recommend you use .NET Framework in lieu of .NET Core or .NET Standard components if possible.

However, it is possible to use .NET Core components with FoxPro and wwDotnetBridge. But the process of doing so tends to be more complicated as .NET Core's assembly loading is more complex, often requiring many more support assemblies that are not always easy to identify.

In order to use .NET Core you need to ensure a matching .NET Core runtime is installed to support the minimum version of any components you are calling. .NET Core goes back to requiring installable runtimes, rather than having a single system provided runtime as .NET Framework does. This means you have to ensure the right version of the runtime is installed. Although .NET Core also supports fully self contained installs, that's not really applicable to components or FoxPro applications, so we're always dependent on an installed runtime. Major Runtime Versions rev every year, the current version in late 2024 is v8 with v9 getting released in November.

The good news is that most components today still use multi-targeting and support both .NET Framework (or .NET Standard which is .NET Framework compatible) and .NET Core targeting and you can generally find .NET Framework components that work more easily in FoxPro.

Stick to .NET Framework

Bottom Line: If at all possible aim for using .NET Framework if you're calling .NET code from FoxPro. Only rely on .NET Core components if there is no alternative in .NET Framework available.

What is wwDotnetBridge?

wwDotnetBridge is an open source, MIT licensed FoxPro library, that allows you to load and call most .NET components from FoxPro. It provides registrationless activation of .NET Components and helpers that facilitate accessing features that native COM Interop does not support.

The key features are:

  • Registrationless access to most .NET Components
    Unlike native COM Interop, you can instantiate and access .NET Components and features, without requiring those classes to be registered as COM objects. Objects are instantiated from within .NET, so you can access most .NET components by directly loading them from their DLL assembly. Both .NET Framework (wwDotnetBridge) and .NET Core (wwDotnetCoreBridge) are supported.

  • Instantiates and Interacts with .NET Objects via COM from within .NET
    wwDotnetBridge is a .NET based component that runs inside of .NET and acts as an intermediary for activation, invocation and access operations. A key feature is that it creates .NET instances from within .NET and returns those references using COM Interop. Once loaded you can use all features that COM supports directly: Property access and method calls etc. as long the members accessed use types that are supported by COM.

  • Support for Advanced .NET Features that COM Interop doesn't support
    Unfortunately there are many .NET features that COM and FoxPro don't natively support directly: Anything related to .NET Generics, overloaded methods, value types, enums, various number types to name just a few. But because wwDotnetBridge runs inside of .NET, it provides automatic conversions and helpers to allow access to these features via intermediary Reflection operations. These helpers access the unsupported COM operations from inside of .NET and translate the results into COM and FoxPro compatible results that are returned into your FoxPro application.

  • Automatic Type Conversions
    Because there are many incompatible types in .NET that don't have equivalents in COM or FoxPro, wwDotnetBridge performs many automatic type conversions. These make it easier to call methods or retrieve values from .NET by automatically converting compatible types. For example: decimals to double, long, byte to int, Guid to string etc. There are also wrapper classes like ComArray that wraps .NET Arrays and Collections and provides a FoxPro friendly interface for navigating and updating collections, and ComValue which wraps incompatible .NET values and provides convenient methods to set and retrieve the value in a FoxPro friendly way and pass it to .NET methods or property assignments.

  • Support for Async Code Execution
    A lot of modern .NET Code uses async functionality via Task based interfaces, and wwDotnetBridge includes a InvokeTaskMethodAsyc() helper that lets you call these async methods and receive results via Callbacks asynchronously. You can also run any .NET synchronous method and call it asynchronously using InvokeMethodAsync() using the same Callback mechanism.

There's much more, but these are the most common features used in wwDotnetBridge.

A quick Primer

Before we jump in with more explanations lets walk through a simple example that shows how to use wwDotnetBridge in a simple yet common scenario.

I'm going to use a pre-built sample component from a library that's part of the samples called wwDotnetBridgeDemo.dll which is one of the simplest things and also very common things we can do.

Setting up wwDotnetBridge

The first step is that you need wwDotnetBridge. wwDotnetBridge comes in two versions:

  • Free Open Source Version
    This version is available for free with source code from GitHub. You can go to the repo and copy the files out of the /Distribution folder. Copy these files into your FoxPro application path.
    uses CrlLoader.dll as the Win32 connector

  • Commercial Version in Web Connection and West Wind Client Tools
    This version of wwDotnetBridge includes a few additional convenience features and .NET components that are not provided in free version. The core feature set however is identical. Unlike the open source version this version uses wwIPstuff.dll as the loader.
    uses wwIPstuff.dll as the Win32 connector

The three files you need for wwDotnetBridge are:

  • CrlLoader.dll (OSS) or wwIPstuff.dll (Commercial)
  • wwDotnetBridge.dll
  • wwDotnetBridge.prg

Copy these into your root project folder. CrlLoader.dll or wwIPstuff.dllhave to live in the root folder the other two can live along your FoxPro path.

For all examples in this article I use the GitHub repo's root folder as my base directory from which to run the samples. There's a .\bin folder that contains all .NET assemblies and for this sample I'll use the bin\wwDotnetBridgeDemos.dll assembly.

I'll start with the entire bit of code and we'll break it down afterwards:

*** Set Environment - path to .\bin and .\classes folder
DO _STARTUP.prg

*** Load wwDotnetBridge
do wwDotNetBridge                 && Load library
LOCAL loBridge as wwDotNetBridge  && for Intellisense
loBridge = GetwwDotnetBridge()    && instance

*** Load an Assembly
? loBridge.LoadAssembly("wwDotnetBridgeDemos.dll")

*** Create an class Instance
loPerson = loBridge.CreateInstance("wwDotnetBridgeDemos.Person")

*** Access simple Properties - plain COM
? loPerson.Name
? loPerson.Company
? loPerson.Entered

*** Call a Method
? loPerson.ToString()
? loPerson.AddAddress("1 Main","Fairville","CA","12345")

*** Special Properties - returns a ComArray instance
loAddresses = loBridge.GetProperty("Addresses")  
? loAddresses.Count     && Number of items in array
loAddress = loAddresses.Item(0)
? loAddress.Street
? loAddress.ToString()

Loading wwDotnetBridge and Loading your first .NET Library

The first step is to load the FoxPro wwDotnetBridge class:

do wwDotNetBridge                 && Load library
LOCAL loBridge as wwDotNetBridge  && for Intellisense
loBridge = GetwwDotnetBridge()    && instance

The first line loads the library into FoxPro's procedure stack so that they library is available. Typically you'd do this at the top of your application once. The LOCAL declaration is optional and only done for Intellisense.

GetwwDotnetBridge() is a helper function that creates a cached instance of wwDotnetBridge that stays loaded even if loBridge goes out of scope. This is to minimize the overhead of having to reload or check to reload the .NET Runtime.

Loading Assemblies and Creating an Instance

Next you want to load an assembly (a DLL) which loads the functionality of that library and makes it accessible so that we can access functionality in it.

*** Load a .NET Assembly
if !loBridge.LoadAssembly("wwDotnetBridgeDemos.dll") 
   ? "ERROR: " + loBridge.cErrorMsg
   RETURN
endif   

LoadAssembly is used to load a DLL from the current folder or any folder in your FoxPro path. LoadAssembly returns .T. or .F. and you can check the .cErrorMsg for more information if an error occurs.

Once the assembly is loaded you can create object within it. In this case I want to create a Person class:

*** Create an class Instance
loPerson = loBridge.CreateInstance("wwDotnetBridgeDemos.Person")
IF VARTYPE(loPerson) # "O"
   ? "ERROR: " + loBridge.cErrorMsg
   RETURN
ENDIF

The .CreateInstance() method takes a fully qualified type name which corresponds to the .NET namespace.classname (case sensitive). A namespace is an identifier that's use to separate types with the same name from each other so that if two vendors have components with the same name, they are still separated by their namespaces.

Again you can check for errors of the instance creation with a non-object type and you can use .cErrorMsg to get more information on the error.

Common errors for both of these methods are:

  • Invalid Names - including case. Make sure you name things EXACTLY
  • Missing dependencies (more on that later)

Direct Invocation of Members

If the class was created you have now received back a .NET Component in the form of a COM object and you can access any properties and methods that have simple types directly. Essentially any properties and methods that contain COM compatible types can be accessed directly.

So all of the following works:

*** Access simple Properties - plain COM
? loPerson.Name
? loPerson.Company
? loPerson.Entered

loPerson.Name = "Jane Doe"
loPerson.Company = "Team Doenut"
loPerson.Entered = DateTime()

Likewise you can call simple methods:

loPerson.ToString()
? loPerson.AddAddress("1 Main","Fairville","CA","12345")

because all of these operations work with simple types they just work with direct access to the .NET object. You can directly call the methods and access properties without any special fix up or proxying.

Proxy Invocation for Problem Types and Helpers

While simple types work just fine for direct access, .NET has many more types that are not compatible with COM or FoxPro types and can't be converted. This means direct access is simply not possible.

wwDotnetBridge provides a host of helper functions that effectively proxy indirect access to .NET functionality by translating data coming from FoxPro into .NET and vice versa. By hosting this proxy code inside of .NET, the proxy can access all features of .NET and - as best as possible - translate between the .NET and FoxPro incompatibilities.

In the example above, the loPerson.Addresses property is an array, a .NET collection type. Collection types and especially generic lists and dictionaries (List<T> or Dictionary<TKey, TValue> ) are very common in .NET for example. However, FoxPro and COM don't support Generics at all, and even basic arrays and lists are not well supported via COM interop. In addition, Value types, Enums or accessing any static members is not supported, but can be accomplished via wwDotnetBridge helpers.

So loPerson.Addresses is an array of address objects, so rather than directly accessing it via:

loAddress = loPerson.Addresses[1]; && doesn't work

you have to indirectly access the address array which returns a ComArray helper:

loAddresses = loBridge.GetProperty(loPerson,"Addresses");
lnCount = loAddresses.Count
loAddress = loAddress.Item(0);
? loAddress.Street
? loAddress.ToString()

GetProperty() is one of the most common helper methods along with InvokeMethod() and SetProperty(). Use these methods when direct access does not work or when you know you're dealing with types that don't work via FoxPro or COM.

These methods use Reflection in .NET to perform their task and you specify an base instance that the operation is applied to (ie. loPerson) and a Member that is executed as a string (ie. "Addresses").

Here's what InvokeMethod looks like:

lcName = "Rick"
loPerson.InvokeMethod(loPerson,"DifficultMethod", lcName) 

There are also methods to invoke static members:

? loBridge.InvokeStaticMethod("System.String","Format", "Hello {0}", lcName)

There are many more helpers in the class and we'll see more of them in the examples in the later part of this article.

How does wwDotnetBridge Work

wwDotnetBridge acts as an intermediary between FoxPro and .NET. In a nutshell, wwDotnetBridge is a loader for the .NET framework, and a proxy interface for FoxPro which allows FoxPro code to pass instructions into .NET code when native direct access to components is not supported. You get the best of both worlds: Native direct COM access when possible, and proxied indirect execution that translates between .NET and COM/FoxPro types to access features that otherwise wouldn't be available.

Here's a rough outline of how wwDotnetBridge loads and calls a .NET component:

A .NET Loader

The most important feature of wwDotnetBridge is that it acts as a loader that makes it possible to access most .NET types - including static members - from FoxPro. There's no registration required as wwDotnetBridge loads components from within .NET and passes them back to FoxPro.

wwDotnetBridge works like this:

  • It loads the .NET Runtime into the FoxPro Process (one time)
  • It bootstraps a .NET wwDotnetBridge.dll from the loader (one time)
  • wwDotnetBridge is the FoxPro ? .NET proxy interface (one time)
  • The loaded proxy instance is passed back to FoxPro (one time)
  • The FoxPro instance is then used to:
    • Load .NET Assemblies (dlls) (once per library)
    • Instantiate .NET Types
    • Access 'simple' properties and methods directly via COM
    • Invoke problem Methods and access problem Properties via indirect execution
    • Convert specialty types that COM doesn't work with directly via specialty helpers

Figure 1 show what this looks like in a diagram:

Load Flow
Figure 1 - wwDotnetBridge: Loading a component, creating an instance and accessing members repeatedly

Let's break down the parts:

In order to execute .NET code from FoxPro, the .NET Runtime needs to be loaded into your FoxPro executable or the FoxPro IDE.

There are a couple of ways this can be done:

  • Native COM Interop via COM Instantiation
    .NET has native support for COM instantiation, but it requires that .NET Components are explicitly marked for COM execution which very few are. COM components that you create yourself also have to be marked with explicit COM Interface markers, and have to be registered using a special .NET COM Registration tool. It works, but it's very limited.
    not recommended any longer

  • wwDotnetBridge .NET Runtime Hosting
    wwDotnetBridge includes a small Win32 API loader that bootstraps the .NET Runtime and loads itself into this runtime as the host loader assembly. This is similar to the way native COM Interop hosts the runtime, but it bootstraps the wwDotnetBridge .NET component that can directly instantiate .NET types without requiring COM registration or any special marker interfaces/attributes.

wwDotnetBridge Loader

The wwDotnetBridge is comprised of three components:

  • A Win32 native C++ Loader that loads the .NET Runtime
  • The wwDotnetBridge .NET Proxy class that acts as the bridge interface
  • The wwDotnetBridge FoxPro class that calls into the .NET Class to create classes, invoke methods and set/get properties etc.

The FoxPro class kicks off the process by instantiating the wwDotnetBridge FoxPro class which internally calls into the Win32 code to check and see whether the .NET Runtime is already running and if not starts it up and bootstraps the wwDotnetBridge .NET class into it. A reference to the .NET Class instance is then marshalled back into FoxPro over COM. The FoxPro class caches this COM references and can now make calls into the .NET Interface.

Once loaded the wwDotnetBridge FoxPro class can then communicate with the .NET class by loading assemblies, creating instances and accessing members - in essence it now has full access to the features of the wwDotnetBridge .NET interface.

Direct Member Access is via COM

Behind the scenes .NET Interop relies on COM to instantiate types. Both native COM interop and wwDotnetBridge instantiate or access .NET types and those types return their results over COM. Every call that is made to create an instance, access a method or set or get a property happens over COM regardless of whether you use direct access of objects, or you use wwDotnetBridge's helpers.

Direct COM access is obviously easiest and most efficient from FoxPro and it works with types that are supported by both COM and FoxPro. It works just like you'd expect: You can call methods and access properties directly on the object instance or use the value as is. This works for all 'basic' types, strings, integers, floats, dates, bool values etc. and as long as methods only pass and return these compatible types or properties you can directly access them. Easy!

Indirect Member Access via InvokeMethod(), GetProperty() and SetProperty()

But... .NET has a rich type system and certain types and operations don't translate to matching COM or FoxPro types. For example, you can't access static members via COM but you can with wwDotnetBridge. For problem type access wwDotnetBridge automatically provides translations for many types so that they can be used from FoxPro. For example long and byte are converted to ints, Guid is converted to string, DbNull (a COM null) is converted to null and so on.

For these 'problem' scenarios wwDotnetBridge supports indirect invocation which executes operations through the wwDotnetBridge .NET Component, which proxies the method call or property access from within .NET. Because the code executes within .NET it can use all of .NET's features to access functionality and then translate results in a way that COM and FoxPro supports.

The most commonly used methods are:

  • InvokeMethod() - Invokes a method on an object
  • GetProperty() and SetProperty() - Gets or sets a property on an object
  • InvokeStaticMethod() - call a static method by name
  • SetProperty() and GetStaticProperty() - gets or sets a static property by name

These methods automatically translate inbound and outbound parameters so that they work correctly in .NET and FoxPro.

wwDotnetBridge Proxy Type Wrappers: ComArray and ComValue

wwDotnetBridge provides wrapper types like ComArray and ComValue that automatically wrap types that are not directly supported via COM. ComArray is automatically returned for .NET Arrays, Lists and Collections and it provides the ability to access list items (via Items(n)) and the ability manipulate the list by adding, editing and removing items without the collection ever leaving .NET. Many collection types - especially Generic Ones - cause errors if any member is accessed at all, and so leaving the type in .NET makes it possible to manipulate it and still pass it to methods or assign it to a property.

ComValue is similar, and it provides a mechanisms for storing a .NET value with helper methods that convert to and from FoxPro types to extract and set the .NET stored value.

Both ComValue and ComArray can be passed to the indirect calling methods, and are automatically translated into their underlying types when passed. So if a method requires a List<T> parameter, you can pass a ComArray and InvokeMethod() fixes up the ComArray into the required List<T> typed parameter.

These features and helpers make it possible to access most of .NET's functionality from FoxPro.

Figuring out what's available in .NET

One of the biggest hurdles to using .NET from FoxPro is that you have to figure out what's available, which components you need and what you can call in a .NET component.

Let's look at these 3 scenarios:

  • How do I get the DLLs I need
  • What dependencies are there
  • What methods and properties are there to access

Figuring out what Assemblies (DLLs) to Provide

First and foremost you need to figure out what components you want to use. There are typically three kinds:

  • Built-in .NET Framework components (built-in)
  • Third Party libraries provided as binary files (dlls)
  • Third Party or Microsoft Components that are distributed as NuGet packages (NuGet Packages ? dlls)

Raw DLL Distribution

In the old days of .NET there were only DLLs. Everything was either in the GAC, or distributed as a DLL that was attached as a reference to a project. That still works even in modern .NET although raw DLL usage tends to be rare these days as most components are shared as NuGet packages.

Built-in .NET Components that are part of the core framework are easiest as they are built into the .NET framework or they are located in the Global Assembly Cache. Most of these components are pre-loaded as part of the runtime, so you don't even need to call .LoadAssembly() to first load an assembly. Those that aren't pre-loaded can just be referenced by their assembly name.

This is obviously easiest because you don't really have to figure out where the DLLs are coming from. To figure out where runtime files live you can look at the .NET documentation - for a specific class. Each class tells you which namespace and the DLL file name.

The next option is to provide the Dll files for you to use. This is not very common any more as NuGet has taken over that space, but it's still relevant especially when you build your own .NET Components and distribute them. When you build your own components you generally build them into a DLL and then either build directly into the folder where they are needed or you copy them over to that location. At that point you just use .LoadAssembly() and you're on your way.

NuGet Package Distribution

These days the most common way .NET components are distributed is via NuGet packages - and for good reason. By publishing a component as a package an author can make that package available on the NuGet site and the component can be easily shared and accessed by millions of .NET developers as any component shows up in the NuGet package directory.

NuGet Packages are Zip files with .nupkg extension, that contain the published package Dlls, potentially for multiple .NET target platforms (ie. net472, net8.0). But the NuGet package Zip file does not contain any of its dependencies. Instead dependencies are resolved and unpacked as part of the .NET Build process using either dotnet build or dotnet restore in the SDK.

FoxPro doesn't have NuGet support, so we have to first manually 'unpack' NuGet packages and their dependencies in a way that we can capture all the required DLLs including the dependencies and add them to our own projects.

Make sure Dependencies are provided

It's crucially important that when you reference an assembly (dll) that all of its dependencies - and the right versions thereof - are also available in the same location or the app's root folder. The .NET loader has to be able to find the dependencies.

To give you an idea of a dependency graph, here's an example we'll use later on Westwind.Ai which talks to OpenAI services:

Nuget Dependencies
Figure 2 - NuGet packages can have dependencies that need to be available in order for a component to work.

Notice that this Westwind.AI package has a dependency on Westwind.Utilities which in turn has a dependency on Newtonsoft.Json. All three of these are required to load the component - miss one and the component won't load or break later at runtime when the relevant library is used. Make sure you have all of the dependencies available.

Note that many .NET packages don't have external dependencies, and in that case you can actually unzip and grab the DLLs directly. Just be sure that no other dependencies are required as it's often hard to debug assembly load errors after the fact only to find out you're missing a nested assembly.

Finding DLLs: Unpacking NuGet Packages and their Dependencies using a .NET Project

If you have a NuGet package and you want to 'unpack' it and all of its dependencies there's a 'proper' way of doing it to ensure you capture all dependencies. To resolve all of these dependencies into DLLs we can actually use with wwDotnetBridge in FoxPro, I recommend that you install the .NET SDK and build a tiny project that loads the NuGet package. That creates build output, along with all the dependent DLLs that you can then copy into your FoxPro project folder.

The SDK is a one time, low impact install and it's required in order to build .NET code.

To do this:

  • Create a new Console project by copying .csproj and program.cs files
  • Add a package reference to the desired component
  • Run dotnet build
  • Go to the /bin/Release/net472 folder
  • Pick up the DLLs and copy to your application
  • Or: reference DLLs in that folder

I use a Console project because it's most lightweight top level executable project you can build.

The project file can be totally generic so lets do it just by creating a couple of files:

  • Create a folder name it ConsoleTest
  • Create a file called ConsoleTest.csproj
  • Create a file called program.cs
  • Run dotnet build from that folder

Here's the content of these files. The .csproj project file looks like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Westwind.AI" Version="*" />    
    <!--<PackageReference Include="GoogleAuthenticator" Version="*" />-->
    <!--<PackageReference Include="WeCantSpell.Hunspell" Version="*" />-->
  </ItemGroup>
</Project>

Put the file into its own folder like ConsoleTest and name the project ConsoleTest.csproj.

Also create a file called program.cs file as the main program:

using System;

namespace ConsoleStart {
    public class Program 
    {
        public static void Main(string[] args) {
            Console.WriteLine("Hello World!");
        }
    }
}

dotnet new doesn't work for .NET Framework Projects

The SDK has built-in tools to create projects using dotnet new console -n ConsoleTest, but unfortunately newer SDK versions no longer support .NET Framework, even though it does support compilation of .NET Framework code. The problem is that new project types include a number of C# language features that are not supported in .NET Framework, so rather than removing and changing settings, it's simply easier to create or copy the files manually as shown above.

You can find the above template in the /dotnet/New Project net472 Template. To create a new project, copy the folder and rename the folder and project name and you're ready to go.

With these files in place and the .NET SDK installed, open a Terminal window in the ConsoleTest folder and run:

dotnet build

Output gets generated into the ./bin/Debug/net472 folder. Here's what that looks like using Visual Studio Code as the editor with its built-in Terminal (note: you can use whatever editor you like).

Dotnet Build For Dependencies
Figure 3 - Building a tiny .NET project with the NuGet package you want to use, produces all DLLs and dependencies you can copy into your own project.

As you can see the output compilation folder contains all 3 of the assemblies from the 3 NuGet packages and you can now copy those DLLs into your application folder.

You can create this project once, and simply add different package references into the project, delete the bin folder, then build again to figure out what dependencies you need to deploy:

Use a Disassembler Tool to discover .NET Types and Members

Once you have the DLLs and you can use them successfully with LoadAssembly() the next step is to find the types (classes) you want to load and the members you want to call on them.

There are many tools available that provide IL code inspection that can show you class hierarchies in a DLL:

Each tool is a little different, but most modern tools use live decompilation to show the class tree and member signatures. Figure 2 shows the Markdig Markdown library in ILSpy:

Inspecting Class Signatures
Figure 4 - Inspecting .NET Libraries for call signatures and class naming in ILSpy

There are a number of things to be aware of:

  • Classes are instantiated by using namespace.classname
  • All type names and members are case sensitive
  • All methods require exact parameter matches (default values are not supported over COM)
  • Overloaded methods cannot be called directly
  • If there are overloads make sure you use the exact parameter types

The above ToHtml() method - which happens to be a static method that doesn't require an objec instance - would be called like this from FoxPro:

loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly("Markdig.dll")
lcHtml = loBridge.InvokeStaticMethod("Markdig.Markdown","ToHtml",lcMarkdown,null)

Use LinqPad to Test Out Code

Another extremely useful tool is LinqPad which you can think of as a Command Window for .NET. Like the FoxPro Command Window you can use LinqPad to test out code interactively. If you didn't create the component you're trying to access from .NET it's a good idea to try it out before you actually try to call it from FoxPro.

This is useful for several reasons:

  • Makes sure you are calling the code correctly, and it works in .NET
    if it doesn't work in .NET, it sure as hell won't work in FoxPro!
  • Lets you check for type information interactively
    hover over type declaration to see the namespace.classname and class
  • Lets you see overloads and test with different values/types
    hover over a method and check for a drop down list (n overloads)

There are two versions of LinqPad available for different scenarios:

  • LinqPad 5.0 works with .NET Framework
  • LinqPad Latest (8.0 currently) works with .NET Core

Although in many cases both .NET Framework and Core work the same there are differences, so if you're testing for FoxPro you generally prefer using LinqPad 5.0 to test the .NET Framework versions of components.

Here's LinqPad checking out the Spell Checking example code:

LinqPad
Figure 5 - Using LinqPad to test out .NET functionality before creating FoxPro code.

LinqPad is an awesome tool if you're using .NET in general - it allows you to create small snippets for testing as I've shown, but you can also use it as a tool to create small utilities like converters, translators and general purpose tools that you can readily save and then later load. For example, I have several converters that convert TypeScript classes to C# and vice versa, de-dupe lists of email addresses and many other things that are basically stored as LinqPad scripts that I can pull up and tweak or paste different source text into.

Usage Examples

Ok enough theory - let's jump in and put all of this into practice with some useful examples that you can use in your own applications.

  1. wwDotnetBridge 101 – Load, Create, Invoke, Get/Set
  2. Create a powerful String Formatter
  3. Add Markdown Parsing to your Applications
  4. Use a Two-Factor Authenticator Library
  5. Add Spellchecking to your applications
  6. Humanize numbers, dates, measurements
  7. File Watcher and Live Reload (Event Handling)
  8. Async: Use OpenAI for common AI Operations
  9. Async: Print Html to Pdf
  10. Create a .NET Component and call it from FoxPro

wwDotnetBridge 101 – Load, Create, Invoke, Get/Set

Demonstrates:

  • Basics of wwDotnetBridge
  • Loading the library
  • Loading assemblies
  • Creating instances
  • Accessing members
  • Using special unsupported types

Lets start with a basic usage example that demonstrates how wwDotnetBridge works.

For this 101 level example I'm going to use a custom class in custom compiled project I created for examples for this session. We'll talk about how to create this class later, but for now just know that this project creates an external .NET assembly (.dll) from which we'll load a .NET class, and call some of its members.

Specifically we'll look at how to:

  • Load wwDotnetBridge
  • Load an Assembly
  • Create a .NET Object instance
  • Make native COM calls on the instance
  • Invoke or access problem members on an instance
  • Use Helper Classes to work with problematic .NET Types

Simple Invocation

The easiest way to look at this is to look at commented example.

do wwDotNetBridge                 && Load library
LOCAL loBridge as wwDotNetBridge  && for Intellisense only
loBridge = GetwwDotnetBridge()    && Create Cached Instance of wwDotnetBridge

*** Load an .NET Assembly (dll)
loBridge.LoadAssembly("wwDotnetBridgeDemos.dll")

*** Create a class Instance - `Namespace.Classname`
loPerson = loBridge.CreateInstance("wwDotnetBridgeDemos.Person")

*** Access simple Properties - direct access
? "*** Simple Properties:" 
? loPerson.Name
? loPerson.Company
? loPerson.Entered
?

*** Call a Method - direct access
? "*** Method call: Formatted Person Record (ToString):"
? loPerson.ToString()  && Formatted Person with Address
?

*** Add a new address - direct access
loAddress =  loPerson.AddAddress("1 Main","Fairville","CA","12345")

*** Special Properties - returns a ComArray instance
loAddresses = loBridge.GetProperty(loPerson, "Addresses")  

? loBridge.ToJson(loAddresses, .T.)  && Commercial only
? TRANSFORM(loAddresses.Count) + " Addresses"     && Number of items in array

? "*** First Address"
loAddress = loAddresses.Item(0)
? "Street: " + loAddress.Street
? "Full Address (ToString): " + CHR(13) + CHR(10) + loAddress.ToString()
? 

? "*** All Addresses"
FOR lnX = 0 TO loAddresses.Count-1
	loAddress = loAddresses.Item(lnX)
	? loAddress.ToString()
	?
ENDFOR

The first steps are pretty straight forward: You create an instance of the wwDotnetBridge object, which you then use to create an instance of a .NET class - or you can also call static methods directly (using .InvokeStaticMethod() more on that in the next sample).

Once you have the class you can call its methods and access its properties. For any properties and method signatures that are COM compliant, you can just directly access them the same way as you would for FoxPro members.

Indirect Execution

For problem types or some complex types likes arrays and collections, you have to use wwDotnetBridge's indirect invocation methods to access members. The three most common methods are:

  • InvokeMethod()
  • GetProperty()
  • SetProperty()

In this example, the loPerson instance includes an Addresses property which contains an array of Address object. While you can retrieve the Addresses object directly, you can't do anything useful with the array in FoxPro.

So rather than returning the array .GetProperty() returns you a ComArray instance instead which lets you access and manipulate the collection:

*** Returns a ComArray instance
loAddresses = loBridge.GetProperty(loPerson,"Addresses")

? loAddresses.Count   && 2
loAddress1 = loAddresses.Item(0)

FOR lnX = 0 to loAddresses.Count -1 
    loAddress = loAddresses.Item(lnX)
    * ? loAddress.Street
    ? loAddress.ToString()
ENDFOR

loNewAddress = loBridge.CreateInstance("wwDotnetBridge.Address")
loNewAddress.Street = "122 Newfound Landing"
loAddressses.Add(loNewAddress)

loAddresses.Count   && 3

Using ComArray for .NET Arrays, Lists and Collections

Because arrays and collections are ultra-common in .NET here's how you can add a new item to the collection using the same ComArray structure:

? "*** Add another item to the array"
* loNewAddress = loBridge.CreateInstance("wwDotnetBridgeDemos.Address")
loNewAddress = loAddresses.CreateItem()
loNewAddress.Street = "122 Newfound Landing"
loNewAddress.City = "NewFoundLanding"
loAddresses.Add(loNewAddress)

? TRANSFORM(loAddresses.Count) + " Addresses"  && 3
FOR lnX = 0 to loAddresses.Count -1 
    loAddress = loAddresses.Item(lnX)
    ? loAddress.ToString()
    ? 
ENDFOR

Summary

You've just seen how to:

  • Load a .NET Assembly
  • Create a .NET Class from within it
  • Call methods and set properties
  • Access a complex property and use a helper object
  • Work .NET Collections from FoxPro

Create a powerful String Formatter

Demonstrates:

  • Using .NET native String functions to format strings
  • Calling native .NET methods on objects without assembly loading
  • Invoking static methods
  • Creating simple wrapper functions for .NET functionality

This example is a little more practical: It makes most of .NET's string formatting features available to Visual FoxPro and exposes these as easy to use FoxPro functions. .NET has built-in string formatteing support that allow powerful formatting of things like dates and numbers along with C style string format templates.

In this example we'll access two native .NET features:

  • ToString()
    This method is a base method on the lowest level .NET object which is System.Object and ToString() exists on every object and value in .NET except null. Each type can implement a custom implementation relevant to the type, or
  • System.String.FormatString()
    Is a C-Style template string expansion method, that can be used to embed values more easily into strings using {n} value expansion. Additionally FormatString() supports the same format specifiers that ToString() supports on any templated values.

Formatting Dates and Numbers or any Formattable .NET Type with ToString()

The .NET System.Object base class exposes a ToString() method which ensures that every .NET object and value (except null) has a ToString() method, which allows you to write out any object as a string. Most common .NET types have practical ToString() implementations so that a number will write out the number as string, and date writes out in a common date format. More complex objects have custom ToString() implementations, and if you create your own classes you can override ToString() with your own string representation that makes sense.

Additionally ToString() supports an optional format specifier for many common types, which is specifically useful for numbers and dates since these can be represented in so many different ways.

ToString() with a format specifier is similar in behavior to Transform() in FoxPro, except that .NET formatters tend to be much more flexible with many more options.

The most common formatters are Date and Number formatters, but many other types also have formatters. To do this I'll implement a FormatValue() function in FoxPro shown after the examples.

Let's look at some Date formatting first:

do _startup.prg

do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()

*** No Format String - Default ToString() behavior
? "Plain FormatValue on Date: "  + FormatValue(DATETIME())
* 6/6/2016 7:49:26 PM

lcFormat = "MMM d, yyyy"
? lcFormat + ": " +  FormatValue(DATETIME(),lcFormat)
* Jun 10, 2016

lcFormat = "MMMM d, yyyy"
? lcFormat + ": " + FormatValue(DATETIME(),lcFormat)
* August 1, 2016

lcFormat = "HH:mm:ss"
? lcFormat + ": " + FormatValue(DATETIME(),lcFormat)
* 20:15:10

cFormat = "h:m:s tt"
? lcFormat + ": " +  FormatValue(DATETIME(),lcFormat)
* 8:5:10 PM

lcFormat = "MMM d @ HH:mm"
? lcFormat + ": " +  FormatValue(DATETIME(),lcFormat)
* Aug 1 @ 20:44

lcFormat = "r"  && Mime Date Time
? lcFormat + ": " +  FormatValue(DATETIME(),lcFormat)
* Mon, 06 Jun 2016 22:41:33 GMT

lcFormat = "u"  
? lcFormat + ": " +  FormatValue(DATETIME(),lcFormat)
* 2016-06-06 22:41:44Z

lcFormat = "ddd, dd MMM yyyy HH:mm:ss zzz"
? "MimeDateTime: " +  STUFF(FormatValue(DATETIME(),lcFormat),30,1,"")
* 2016-06-06 22:41:44Z

There are a lot of different time formats available including fully spelled out versions. By default all date formats are in the currently active user locale (ie. en_US or de_DE) and the value will adjust based on which language you are running your application in. It's also possible to pass a specific .NET Culture to format for some other language and formatting, but that's not supported for the helpers discussed here.

Number formatting is very similar:

? "*** Numberformats"

*** Number formats

lcFormat = "00"  && fills with leading 0's
? lcFormat + ": " + FormatValue(2,"00")
* 02

? lcFormat + ": " + FormatValue(12,"00")
* 12

lcFormat = "c"    && currency (symbol, separator and default 2 decimals
? lcFormat + ": " +  FormatValue(1233.22,lcFormat)
* $1,233.22

lcFormat = "n2"   && separators and # of decimals
? lcFormat + ": " +  FormatValue(1233.2255,lcFormat)
* $1,233.23

lcFormat = "n0"   && separators and no decimals
? lcFormat + ": " +  FormatValue(1233.2255,lcFormat)
* $1,233
?

To implement the above FormatValue() function, I use a simple FoxPro wrapper function that looks like this:

************************************************************************
*  FormatValue
****************************************
***  Function: Formats a value using .NET ToString() formatting
***            for whatever the text ends up with
***      Pass:  Pass in any .NET value and call it's ToString()
***             method of the underlying type. This 
***             Optional FormatString ("n2", "MMM dd, yyyy" etc)
***    Return: Formatted string
************************************************************************
FUNCTION FormatValue(lvValue,lcFormatString)
LOCAL loBridge 

IF ISNULL(lvValue)
   RETURN "null"
ENDIF   

loBridge = GetwwDotnetBridge()

IF EMPTY(lcFormatString)	
	RETURN loBridge.InvokeMethod(lvValue,"ToString")
ENDIF  

RETURN loBridge.InvokeMethod(lvValue,"ToString",lcFormatString)
ENDFUNC
*   FormatValue

This function works off the value that we are passing into .NET and relies on the fact that .NET treats any value or object as an object. So a Date or Number, Boolean all are objects and we can call ToString(format) on those values. So it's literally a single method call.

If no parameter is passed we just call ToString() without parameters, otherwise we call ToString(format). Note that each overloaded .NET method requires a separate FoxPro call - even if the .NET method has default values for the method. This is because .NET internally looks at full method signatures and default parameter values are not part of the meta data that is used to match the right signature to call so we always have to call the exact signature that we want to use including potentially missing parameters. This can make translating C# code to FoxPro a little more tricky at times and is one of the reasons you should always verify method signatures in a Dissassembler tool like ILSpy or test methods in LinqPad with the full parameter structure.

We'll see this even more vividly in the FormatString() function we'll discuss next as it can take a variable number of parameters.

String Formatting with C Style String Templates

If you're old skool like me, you probably remember printf() from your C Computer Science classes back in the day. Most C style languages have printf() style string formatting functionality where you can 'inject' embeddable values into the string. This is not unlike FoxPro's TextMerge() function, but much more efficient and with the added benefit of the same string formatting available for embedded values as discussed for FormatValue().

Here's what this looks like when called from FoxPro:

? "*** String Formatting"
? FormatString("Hey {0}, the date and time is: {1:MMM dd, yyyy - h:mm tt}","Rick",DATETIME())
?

? "*** Brackets need to be double escaped"
? FormatString("This should escape {{braces}} and format the date: {0:MMM dd, yyyy}",DATE())

You can call FormatString() with a string 'template' that contains {0-n} expressions inside of it and you then pass parameters to the functions to fill in the {n} holes with the parameter values. The numbering is 0 based so you start with {0} for the first parameter.

Additionally you can also apply format strings as described in FormatValue() so you can use {0:MMM dd, yyy} for a Date expansion for example.

Note that FormatString() uses ToString() to format the value, so this works with any kind of object, although many actual objects don't implement it and instead return just the object name as Namespace.Classname.

However, if a class implements a custom ToString()method, it can do any kind of custom formatting - as I did in the wwDotnetBridge101 example and the Person.ToString() method, which outputs a full name and address block as a string:

// Person
public string DisplayName => (Name ?? string.Empty) +  
                             (!string.IsNullOrEmpty(Company) ?  $" ({Company})" : string.Empty);
                               
public override string ToString()
{
    return DisplayName + "\r\n" +                  
           Addresses.FirstOrDefault()?.ToString(); 
}

// Address
public override string ToString()
{
    return Street + "\r\n" + City + "\r\n" + State + " " + Zip;
}

You can then use that in FoxPro simply with:

*** Load loPerson .NET Object
? loBridge.LoadAssembly("wwDotnetBridgeDemos.dll")
loPerson = loBridge.CreateInstance("wwDotnetBridgeDemos.Person")
loPerson.Name = "Rick Strahl"
loAddresses = loBridge.GetProperty(loPerson,"Addresses")
loAddress = loAddresses.Item(0)
loAddress.City = "Anywhere USA"

*** Both of these work
? FormatValue(loPerson)
? loPerson.ToString()

The same also works with FormatString():

? FormatString("Person Object:\r\n{0} and the time is: {1:t}", loPerson, DATETIME())

FormatString() is very powerful and quite useful to quickly create string structures.

FormatString() also supports several C# string escape characters like \r\n and \t although that's not natively supported as .NET treats a foxPro string as is and escapes any special characters. However my implementation adds explicit support for \n\r\t\0 and escape them before passing to .NET (which has its own issue as you can't de-escape those values))

Here's what the FoxPro FormatString() function looks like:

************************************************************************
*  FormatString
****************************************
***  Function: Uses a string template to embed formatted values
***            into a string.
***    Assume:
***      Pass: lcFormat    -  Format string use {0} - {10} for parameters
***            lv1..lv10   -  Up to 10 parameters
***    Return:
************************************************************************
FUNCTION FormatString(lcFormat, lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8,lv9,lv10)
LOCAL lnParms, loBridge
lnParms = PCOUNT()
loBridge = GetwwDotnetBridge()

lcFormat = EscapeCSharpString(lcFormat)

DO CASE 
	CASE lnParms = 2
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1)
	CASE lnParms = 3
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2)
	CASE lnParms = 4
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3)
	CASE lnParms = 5
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4)
	CASE lnParms = 6
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4,lv5)
	CASE lnParms = 7
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4,lv5,lv6)
	CASE lnParms = 8
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7)
	CASE lnParms = 9
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8)
	CASE lnParms = 10
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8,lv9)
	CASE lnParms = 11
		RETURN loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8,lv10)
	OTHERWISE
	    ERROR "Too many parameters for FormatString"
ENDCASE

ENDFUNC
*   StringFormat

************************************************************************
*  EscapeCSharpString
****************************************
***  Function:
***    Assume:
***      Pass:
***    Return:
************************************************************************
FUNCTION EscapeCSharpString(lcValue)

lcValue = STRTRAN(lcValue, "\r", CHR(13))
lcValue = STRTRAN(lcValue, "\n", CHR(10))
lcValue = STRTRAN(lcValue, "\t", CHR(9))
lcValue = STRTRAN(lcValue, "\0", CHR(0))

RETURN lcValue

The first thing you notice here's is that we are calling a static method on the System.String class. Static methods are non-instance method, meaning you don't first create an instance. Instead the methods are static and bound to a specific type. In FoxPro this is similar to a UDF() function or plain function that is globally available. Static methods and properties are referenced by the type name - ie. System.String instead of the instance, followed by the method or member name.

Here we call the static Format method with the format string and a single value as a parameter:

loBridge.InvokeStaticMethod("System.String","Format",lcFormat,lv1)

In this method, you'll notice the requirement to call each of the overloads for each parameter variation, which looks tedious but actually is the most efficient way to call this method. There are other overloads of InvokeStaticMethod() that can be passed an array of parameters, and while that would be cleaner to look at and allow for an unlimited number of parameters, it's less efficient as the array has to be created and parsed on both ends. Passing values directly is significantly faster, and for a low-level utility method like this, it's definitely beneficial to optimize performance as much as possible.

Summary

In this example you learned:

  • Calling native .NET Methods
  • Calling a non-instance Static method
  • How .NET Format Strings and ToString() work

Add Markdown Parsing to your Applications

Demonstrates:

  • Using a third party NuGet library
  • Calling static methods
  • Creating a FoxPro wrapper class to abstract functionality

The next example demonstrates using a Markdown to HTML parser. Markdown is a very useful text format that uses plain text mixed with a few readable text markup expressions that allow create rich HTML document text via plain text input. It can be used in lieu of WYIWYG editors and because it can be rendered very quickly allows you to actually preview content as you type in real time. So rather than typing in a simulated text editor to tries to simulate the final markup, you write plain text with markup simple expressions and look at a preview (or not) to see what the final output would look like.

In short it's a great tool for writing text that needs to be a little more fancy than just a wall of plain text. It's super easy to add bold, italic, lists, notes, code snippets, embed link and images using Markdown.

Some Examples of Markdown Usage

I'm a huge fan of Markdown and I've integrated it into several of my applications:

  • Markdown Monster (a Markdown Editor)

Markdown Monster

  • Help Builder (HTML documentation and HTML preview)
  • West Wind Message Board (used for message text)
  • My Weblog - posts are written in Markdown and rendered to HTML
  • Articles like this one - written in Markdown

Using Markdig for Markdown To HTML Conversion

Let's start with the simplest thing you can do which is to use a 3rd party library and it's most basic, default function to convert Markdown to Html which is sufficient for most use cases.

I'm using an older version of Markdig (v0.15.2) because it has no extra dependencies. Later versions work fine (although the ToHtml() method signature changes) but it requires that you add several additional dependencies of .NET assemblies. The old version has all the features you are likely to need so for FoxPro use this is the preferred version.

Notice that there are two parameters to the Markdig.Markdown.ToHtml() method: The markdown and a markdown parser pipeline that is optional. Remember from FoxPro we always have to pass optional parameters so we can pass the default value of null.

DO wwutils && For Html Preview

do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()

loBridge.LoadAssembly("markdig.dll")

TEXT TO lcMarkdown NOSHOW
# Raw Markdown Sample using the Markdig Parser

This is some sample Markdown text. This text is **bold** and *italic*.

* [Source Code for this sample on GitHub](https://github.com/../markdownTest.PRG)

![](https://markdownmonster.west-wind.com/docs/images/logo.png) 

* List Item 1
* List Item 2
* List Item 3

Great it works!

> #### Examples are great
> This is a block quote with a header
ENDTEXT


***  Actual Markdown Conversion here - Invoke a Static Method
lcHtml = loBridge.InvokeStaticMethod("Markdig.Markdown","ToHtml",;
                                     lcMarkdown,null)

? lcHtml
ShowHtml(lcHtml)  && from wwUtils show in browser unformatted

And that works:

Markdown Test From Fox Pro

We have to load the markdig.dll library, but the key feature of this code is the static method call to:

lcHtml = loBridge.InvokeStaticMethod("Markdig.Markdown","ToHtml",;
                                     lcMarkdown,null)

This method takes the markdown to parse, plus a parameter of a ParserFactory which we have to pass even though the parameter is null. As I often do I first create the code I want to call in LinqPad to test, then call it from FoxPro. Here's the LinqPad test:

Markdig In Linq Pad

And that works.

Adding a more sophisticated Parser Wrapper

The call to ToHtml() in its default form with the parser pipeline set to null gets you a default parser, but you might want to take advantage of additional features of add-ons that the parser supports. For example, you can add support for Github Flavored Markdown (Github specific features), Grid Tables, Pipe Tables, automatic link expansion and much more.

To do this it's a good idea to create a wrapper class and build and cache the pipeline so it can be reused easily.

Here's a Markdown Parser class:

*************************************************************
DEFINE CLASS MarkDownParser AS Custom
*************************************************************
oPipeline = null
oBridge = null

lEncodeScriptBlocks = .T.
lSanitizeHtml = .T.
lNoHtmlAllowed = .F.

************************************************************************
FUNCTION Init()
****************************************
LOCAL loBridge as wwDotNetBridge

loBridge = GetwwDotnetBridge()

this.oBridge = loBridge
IF ISNULL(THIS.oBridge)
   RETURN .F.
ENDIF

IF !loBridge.LoadAssembly("markdig.dll")
   RETURN .F.
ENDIF   

ENDFUNC
*   Init

************************************************************************
FUNCTION CreateParser(llForce, llPragmaLines)
****************************************
LOCAL loBuilder, loValue, loBridge

IF llForce OR ISNULL(this.oPipeline)
	loBridge = this.oBridge
	loBuilder = loBridge.CreateInstance("Markdig.MarkdownPipelineBuilder")

	loValue = loBridge.Createcomvalue()
	loValue.SetEnum("Markdig.Extensions.EmphasisExtras.EmphasisExtraOptions.Default")	
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseEmphasisExtras",loBuilder,loValue)

	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseListExtras",loBuilder)	
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseCustomContainers",loBuilder)

	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseFooters",loBuilder)
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseFigures",loBuilder)
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseFootnotes",loBuilder)
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseCitations",loBuilder)	
	
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UsePipeTables",loBuilder,null)
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseGridTables",loBuilder)

	loValue = loBridge.Createcomvalue()
	loValue.SetEnum("Markdig.Extensions.AutoIdentifiers.AutoIdentifierOptions.GitHub")
	loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseAutoIdentifiers",loBuilder,loValue)
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseAutoLinks",loBuilder)
	
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseYamlFrontMatter",loBuilder)
	loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UseEmojiAndSmiley",loBuilder,.T.)

	IF this.lNoHtmlAllowed
	   loBuilder = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","DisableHtml",loBuilder)
	ENDIF

	IF llPragmaLines
	  loBuiler = loBridge.Invokestaticmethod("Markdig.MarkdownExtensions","UsePragmaLines",loBuilder)
	ENDIF

	THIS.oPipeline = loBuilder.Build()
ENDIF

RETURN this.oPipeline
ENDFUNC
*   CreateParser

************************************************************************
FUNCTION Parse(lcMarkdown, llUtf8, llDontSanitizeHtml)
LOCAL lcHtml, loScriptTokens, loPipeline, lnOldCodePage

IF !this.lEncodeScriptBlocks
   loScriptTokens = TokenizeString(@lcMarkdown,"<%","%>","@@SCRIPT")
ENDIF

loPipeline = this.CreateParser()

*** result always comes back as UTF-8 encoded
IF (llUtf8)
   lnOldCodePage = SYS(3101)
   SYS(3101,65001)
   lcMarkdown = STRCONV(lcMarkdown,9)
ENDIF

lcHtml = this.oBridge.InvokeStaticMethod("Markdig.Markdown","ToHtml",lcMarkdown,loPipeline)

IF llUtf8
  SYS(3101,lnOldCodePage)  
ENDIF

IF !THIS.lEncodeScriptBlocks
  lcHtml = DetokenizeString(lcHtml,loScriptTokens,"@@SCRIPT")
ENDIF

IF PCOUNT() < 3
   llDontSanitizeHtml = !THIS.lSanitizeHtml
ENDIF   

IF !llDontSanitizeHtml
  lcHtml = THIS.SanitizeHtml(lcHtml)
ENDIF

lcHtml = TRIM(lcHtml,0," ",CHR(13),CHR(10),CHR(9))

RETURN lcHTML   
ENDFUNC
*   Parse


************************************************************************
*  SanitizeHtml
****************************************
***  Function: Removes scriptable code from HTML. 
************************************************************************
FUNCTION SanitizeHtml(lcHtml, lcHtmlTagBlacklist)

IF EMPTY(lcHtmlTagBlackList)
	lcHtmlTagBlackList = "script|iframe|object|embed|form"
ENDIF
IF EMPTY(lcHtml)
   RETURN lcHtml	
ENDIF

RETURN THIS.oBridge.InvokeStaticMethod("Westwind.WebConnection.StringUtils","SanitizeHtml",lcHtml, lcHtmlTagBlacklist)
ENDFUNC
*   SanitizeHtml

ENDDEFINE

The key method is the CreateParser() which explicitly adds the features that we want to use with the parser. There are additional methods that help with optionally cleaning up HTML for safe rendering by removing script code and frames and other things that could allow XSS attacks against the rendered HTML as Markdown allows embedded HTML in the Markdown text.

In the samples there's another sublass called MarkdownParserExtended that adds a few more features to the parser that include code snippet parsing, expanding FontAwesomeIcons and a few other things. You can look at the source code for more info.

With this code in place you can now just create another helper method that uses this parser and cache it so we don't have to reload the pipeline and instance for each invocation:

************************************************************************
*  Markdown
****************************************
***  Function: Converts Markdown to HTML
***    Assume: Caches instance in __MarkdownParser
***      Pass: lcMarkdown  - text to convert to HTML from Markdown
***            lnMode      - 0/.F. - standard, 2 extended, 1 - standard, leave scripts, 3 - extended leave scripts
***    Return: parsed HTML
************************************************************************
FUNCTION Markdown(lcMarkdown, lnMode, llReload, llUtf8, llNoSanitizeHtml, llNoHtmlAllowed)
LOCAL loMarkdown, lcClass

IF llReload OR VARTYPE(__MarkdownParser) != "O" 
	IF EMPTY(lnMode)
	   lnMode = 0
	ENDIF   

	lcClass = "MarkdownParser"
	IF lnMode = 2
	   lcClass = "MarkdownParserExtended"
	ENDIF
	
	loMarkdown = CREATEOBJECT(lcClass)
	PUBLIC __MarkdownParser
	__MarkdownParser = loMarkdown
	
	IF lnMode = 1 OR lnMode = 3
	   __MarkdownParser.lEncodeScriptBlocks = .F.  	  	   	  
	ENDIF	
	
	__MarkdownParser.lSanitizeHtml = !llNoSanitizeHtml
	__MarkdownParser.lNoHtmlAllowed = llNoHtmlAllowed
ELSE
    loMarkdown = __MarkdownParser
ENDIF

RETURN loMarkdown.Parse(lcMarkdown, llUtf8)
ENDFUNC
*   Markdown

Using Templates to make the Markdown Look Nicer

Markdown is useful especially in Web applications where HTML can be directly displaying inside of a Web Page. But if you just generate the HTML and display it as is the output is somewhat underwhelming as you're getting the browser's default styling.

If you're using Markdown in desktop applications what you'd want to do, likely is to create an HTML page template into which to render the generated HTML, with CSS styling applied so you can produce output that looks a little more user friendly:

Markdown Template Output

This actually uses styling I picked up from Markdown Monster via templating. This works by creating an HTML template and embedding the rendered markdown - along with some base paths - into it:

Beware of TextMerge()

Initially I used the TextMerge() function to merge text, but it turns out that it has difficulty with linefeeds only which are common with Markdown content created in external editors. For certain things like code snippets the stripped line breaks are causing problems. So rather than using TextMerge() in the code below I'm explicitly placeholder values in the text.

<!DOCTYPE html>
<html lang="en">
<head>
    <base href="${basePath}"/>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <meta charset="utf-8"/>

    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <link href="${themePath}..\Scripts\fontawesome\css\font-awesome.min.css" rel="stylesheet"/>
    <link href="${themePath}Theme.css" rel="stylesheet"/>

    <!-- All this is for Code Snippet expansion -->
    <script src="${themePath}..\Scripts\jquery.min.js"></script>
    <link href="${themePath}..\Scripts\highlightjs\styles\vs2015.css" rel="stylesheet"/>
    <script src="${themePath}..\Scripts\highlightjs\highlight.pack.js"></script>
    <script src="${themePath}..\Scripts\highlightjs-badge.js"></script>
    <script src="${themePath}..\Scripts\preview.js" id="PreviewScript"></script> 
</head>
<body>
  <!-- Optional header for the page -->   
  <div style="padding: 0.7em; background: #444; color: white; font-size: 0.8em">    
    <div style="float: right; color: goldenrod; font-weight: 600">    
      Southwest Fox Conference, 2024
    </div>
    <div>
      <img src="Assets/touch-icon.png" style="max-height: 1.3em; padding-right: 0.4em" />
      <span style="font-size: 1.35em; font-weight: 600; color: goldenrod; font-weight: 600">wwdotnetbridge Revisited</span> 
      <i style="font-size: 0.7em">by Rick Strahl</i>
    </div>    
  </div>

<div id="MainContent">

  <!-- Markdown Monster Content -->
  
  ${htmlContent}
  
  <!-- End Markdown Monster Content -->
</div>

</body> 
</html>

As you can see there are areas to be replaced with ${htmlContent} and ${basePath} and ${themePath} variables.

I then use a ShowWebPage.prg file to render the HTML into the template to get the desired styling and other feature support.

LPARAMETERS lcHTML, lcFile, lcThemePath

************************************************************************
* FUNCTION ShowWebPage
**********************
***  Function: Takes an HTML string and displays it in the default
***            browser. 
***    Assume: Uses a file to store HTML temporarily.
***            For this reason there may be concurrency issues
***            unless you change the file for each use
***      Pass: lcHTML       -   HTML to display
***            lcFile       -   Temporary File to use (Optional)
***            loWebBrowser -   Web Browser control ref (Optional)
************************************************************************
LOCAL lcBasePath, lcThemePath

lcHTML=IIF(EMPTY(lcHTML),"",lcHTML)
lcFile=IIF(EMPTY(lcFile),SYS(2023)+"\ww_HTMLView.htm",lcFile)
lcBasePath = ADDBS(SYS(5) + CURDIR())
lcThemePath = lcBasePath + "Assets\Westwind\"
lcTemplate = "./Assets/Westwind/Theme.html"

IF !EMPTY(lcTemplate)
   lcT =  FILETOSTR(lcTemplate)

   *** TextMerge does weird shit with LineBreaks so do explicit replacements	
   * lcMerged = TEXTMERGE(lcT)
   
   *** For some reason TEXTMERGE strips line feeds
   lcT = STRTRAN(lcT,"${basePath}", lcBasePath)
   lcT = STRTRAN(lcT,"${themePath}", lcThemePath)
   lcHtml = STRTRAN(lcT, "${htmlContent}", lcHtml)
ENDIF

*** Dump to file and preview in Browser
STRTOFILE(STRCONV(lcHtml,9),lcFile)
ShellExecute(lcFile)

RETURN


FUNCTION ShellExecute(tcUrl, tcAction, tcDirectory, tcParms, tnShowWindow)

IF VARTYPE(tnShowWindow) # "N"
   tnShowWindow = 1
ENDIF

IF EMPTY(tcUrl)
   RETURN -1
ENDIF
IF EMPTY(tcAction)
   tcAction = "OPEN"
ENDIF
IF EMPTY(tcDirectory)
   tcDirectory = SYS(2023) 
ENDIF

DECLARE INTEGER ShellExecute  ;
    IN SHELL32.dll as ShellExec_1;
    INTEGER nWinHandle,;
    STRING cOperation,;
    STRING cFileName,;
    STRING cParameters,;
    STRING cDirectory,;
    INTEGER nShowWindow
    
IF EMPTY(tcParms)
   tcParms = ""
ENDIF

RETURN ShellExec_1( _Screen.HWnd,;
                    tcAction,tcUrl,;
                    tcParms,tcDirectory,tnShowWindow)
ENDFUNC
*   ShellExecute

To use this now becomes pretty simple:

loParser = CREATEOBJECT("MarkdownParserExtended")

TEXT TO lcMarkdown NOSHOW
This is some sample Markdown text. This text is **bold** and *italic*.

* List Item 1
* List Item 2
* List Item 3

Great it works!

> #### <i class="fas fa-info-circle" style="font-size: 1.1em"></i>  Examples are great
> This is a block quote with a header


### Link Embedding is easy

* [Sample Repositor](https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited)
* [Source Code for this sample on GitHub](https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited/blob/master/markdownTest.PRG)

### Markdown Monster wants to eat your Markdown!

* [Download Markdown Monster](https://markdownmonster.west-wind.com)

![](https://markdownmonster.west-wind.com/Images/MarkdownMonsterLogo.jpg)

### The Markdown Editor for Windows

ENDTEXT

*** Render the page
lcHtml = loParser.Parse(lcMarkdown)
? lcHtml

*** 
ShowWebPage(lcHtml)

Note that this example is hacked together using the existing Markdown Monster templates, so it it needs a little work to render quite right, but the idea is this:

  • Set up a base HTML page
  • Text Merge any paths and related things into the page
  • Text Merge the rendered Html

Have a fully reusable HTML template that you can easily call to render consistent looking Markdown. This is essentially what I do in Markdown Monster, Help Builder (both desktop apps) as well as on my Weblogs - all use Markdown text rendered into a base template.

Markdown is an extremely powerful tool, and once you use it for a few things it becomes addicitive and you will loathe going back to a full editor like Word for most writing or documentation tasks.

Use a Two-Factor Authenticator Library

Demonstrates:

  • Use a third party library
  • Make simple direct COM calls
  • Very useful to create Two-Factor Auth for Web and Desktop apps
  • NuGet Library used: GoogleAuthenticator

Two factor authentication is a second level of security that you can use in addition to typical username/password or similar login style security. Two-Factor auth serves a second unrelated safety mechanism to verify an account which makes it much harder to hack an account even if passwords are compromised.

There are many mechanisms that can be used for two-factor auth including using SMS messages to verify a user, or using a separate email address. However, a somewhat newer approach uses Authenticator apps that can be used to generate a key generator that can then generate new validation codes that can be checked. The two factor one time passwords use a standard hashing mechanism that is supported by many different tools including third party apps like Google Authentication, Microsoft Authenticator and Authy, as well as password managers like 1Password, LastPass and so on as hardware keys like Yubikey. All of these apps and devices uses the same algorithms to set up and then validate two factor one time codes. Any of these apps or devices can be used but once you create a two-factor setup you have to use the same app or device to validate.

Two-Factor auth is fully self-contained which means there's no need to use a third party service or any special tools beyond some software or hardware device that can handle creating the setup and validation logic.

Two factor authentication is made of two specific steps:

  • Setting up Two-Factor Authentication
    This involves creating a setup key that is maintained by the two-factor app or device. Typically this is displayed as a QR Code that can be scanned with a phone or from a Web app (in desktop Web browser). Alternately this code is also displayed as a numeric key you can manually type into an authenticator or hardware device where QR codes are not supported. To generate a new two-factor setup you typically provide some unique secret identifier that is then later also used to validate two-factor codes.

  • Validating a One Time Passkey
    Once a Two-Factor auth setup has been configured, you then need to validate it. To validate you ask the Authenticator app for a new two-factor one time code and you validate it in combination with your unique application level secret identifier. Based on the two-factor one time code and and the identifier the code can be validated as valid within a given time frame.

Here's what the sample looks like:

Two Factor Qr Code

What I'm presenting here are the tools to do two-factor authentication, not an actual implementation as that's way too complex to tackle here. If you are interested in full two-factor implementation for a Web site see my post on Two-Factor authentication in a .NET Web application.

In a live appplication here's what this looks like.

QR Code Setup

TwoFactor Setup WebApp

As a user you can capture the QR code in an authenticator app like Authy here:

Two Factor Authy

or, more conveniently as part of a password manager like 1Password as I do:

Two Factor ScanQr 1Password

App presents a QR code to initially set up two factor authentication. This is a one time operation where the app generates a new secret key and then stores that secret key with the user/customer record to later use for validation.

Then when a user logs in, they first log in with their username and password, and then are immediately asked for the two-factor one time code as well.

Two Factor Validation In Web

And you can fill that in via your authenticator app, or as I do here with 1Password:

TwoFactor 1Password Validation

Two-Factor Auth with the GoogleAuthentor Library

Unlike the name suggests, this library is not limited to Google Authenticator. It works with any two-factor authenticator like Authy, Microsoft Authenticator and 1Password. The algorithm that is used for two-factor auth hashing is generic so it works with any tool that supports these protocols.

The code to use this library from FoxPro is very simple. The easiest way to use it is to create a small wrapper class:

*************************************************************
DEFINE CLASS TwoFactorAuthentication AS Custom
*************************************************************

oBridge = null
oAuth = null

************************************************************************
FUNCTION Init()
****************************************
this.oBridge = GetwwDotnetBridge()

IF (!this.oBridge.LoadAssembly("Google.Authenticator.dll"))
   ERROR this.oBridge.cErrorMsg
ENDIF  
THIS.oAuth = this.oBridge.CreateInstance("Google.Authenticator.TwoFactorAuthenticator")
IF VARTYPE(THIS.oAuth) # "O"
   ERROR "Failed to load TwoFactorAuthenticator: " + this.oBridge.cErrorMsg
ENDIF

ENDFUNC

*********************************************************************************
FUNCTION GenerateSetupCode(lcApplicationName, lcEmail, lcSecretKey, lnResolution)
****************************************
***  Function: Generates a structure that generates an object containing 
***            a QR code image and manual setup code
***    Assume: Application and Email have no effect on code/qr generation
***      Pass: lcApplicationName  - Name of application
***            lcEmail            - An email address to identify user
***            lcSecretKey        - Secret key tied to the user to identify
***            lnResolution       - larger numbers result in larger CR codes (10)
***    Return: TwoFactorSetup object or null
************************************************************************
LOCAL loAuth

IF EMPTY(lnResolution)
   lnResolution = 10
ENDIF   

loSetupInfo = THIS.oAuth.GenerateSetupCode(lcApplicationName,;
   lcEmail, ;
   lcSecretKey, ;
   .F., lnResolution)
   
loResult = CREATEOBJECT("TwoFactorSetup")
loResult.cQrCodeImageData = loSetupInfo.QrCodeSetupImageUrl
loResult.cSetupKey  = loSetupInfo.ManualEntryKey
loResult.cCustomerSecret = lcSecretKey

RETURN loResult
ENDFUNC

************************************************************************
FUNCTION ValidatePin(lcSecretKey, lcPin)
****************************************
lcPin = STRTRAN(lcPin, " " ,"")
RETURN THIS.oAuth.ValidateTwoFactorPIN(lcSecretKey, lcPin)
ENDFUNC

ENDDEFINE


*************************************************************
DEFINE CLASS TwoFactorSetup AS Custom
*************************************************************

*** Base64 Data Url that contains the image data 

cQrCodeImageData = ""


*** Typable Version of the QrCode data
cSetupKey = ""

*** Unique Customer Key - pass and save with your app
cCustomerSecret = ""

************************************************************************
FUNCTION QrCodeHtml(llImageOnly)
****************************************
IF (llImageOnly)
   RETURN [<img src="] + this.cQrCodeImageData + [" />]
ENDIF

TEXT TO lcHtml NOSHOW TEXTMERGE
<html>
<body>
<div style="text-align: center; max-width: 500px">
	<img src="<<this.cQrCodeImageData>>" />
	<div style="font-size: 1.5em; font-weight: 600">
	<<this.cSetupKey>>
	</div>
</div>
</body>
</html>
ENDTEXT

RETURN lcHtml
* QrCodeHtml

ENDDEFINE
*EOC TwoFactorSetup

The class only has two methods that wrap the basic behavior to create a new Two-Factor setup and to validate it.

Generating and Displaying a QR Setup Code

Let's start with how to generate and then display the QR Code:

*** For demo only (ShowHtml()/InputForm()/GetUniqueId())
DO wwutils

*** Generate a Customer Secret key 
*** In an application this will be something that identifies the user
*** with a given account, but it needs to be something that is not known
*** typically a generated UniqueId stored in a customer record (TwoFactorKey in db for example)
lcSecret = GetUniqueId(12)    

loAuth = CREATEOBJECT("TwoFactorAuthentication")
loSetup  = loAuth.GenerateSetupCode("Westwind.Webstore",;
                                    "rick@test-my-site.com", lcSecret)

ShowHtml( loSetup.QrCodeHtml())

Yeah pretty simple, eh? To start you need some sort of unique ID that you need to store with your application once the two-factor authentication has been enabled. This key is the shared secret between you and the authenticator and it's needed to validate two-factor codes. Ideally you want a random value for this and store it as a seperate field in your user or customer database that identifies a user.

You call GenerateSetupCode() to generate the QrCode or manual setup code which are used initialiaze the Authenticator for your app.

The method returns an loSetup structure that contains the cQrCodeImageData which is HTML base64 encoded image data that can be directly assigned to an <img src="" /> attribute. We can take advantage of that to display the QR code quite easily in an HTML page as shown in the initial examples.

The Setup class includes a mechanism to render the image as HTML into a page that can be displayed:

Two Factor QrCodeRenderedInBrowser

You can see the image tag:

<img src="data:image/png;base64,<longBase64Text>"  />

Alternately if you want to capture the actual image data you can parse the encoded cQrCodeImageData, extract the base64 text and use STRCONV() to turn that into binary data that you can display in an image control which would also work.

The idea with this method is that you do this for setting up your authenticator from the QR code or setup code.

As part of the setup process you'll also need to verify that it's working before you enable two-factor authentication. So a typical Web Form will ask to scan the QR Code and then also provide an initial one time code to verify that it works. Only after that code has been verified should you actually enable two-Factor auth in your application.

Validating a Two-Factor One-Time Code

Once Two-Factor auth has been enabled, you can now log in with your username/password auth and then immediately also check the two factor auth.

loAuth = CREATEOBJECT("TwoFactorAuthentication")

*** Test with a 'known' Customer Secret Key (from my site's test account)
*** In a real app you'd retrieve this from a user/customer record
lcSecret = GETENV("TWOFACTOR_SECRETKEY")

*** Capture Pin (lookup in 1Password, Authy, Google or Microsoft Authenticator, Yubikey etc.
lcPin = InputForm("","Enter generated two-factor pin")
IF EMPTY(lcPin)
   RETURN
ENDIF

*** Check the One Time Code
If(loAuth.ValidatePin(lcSecret,lcPin))
   ? "Pin has been validated"
ELSE
   ? "Invalid Pin code"   
ENDIF

So then I can use my matching user account that matches the secret key, and now use the generated one-time code out of 1Password:

Two Factor Validating In Desktop

I can paste the value and the Authenticator checks for validity.

Two-Factoring: Logic is up to you

The code I've shown provides the logistics, but how you implement is up to you. You can use Two-Factor auth in Web apps where it's quite common, but as you've seen here it's also possible to do this in Desktop applications as long as you can display a QR code - or you can optionally just use the Manual Setup code.

Add Spellchecking to your applications

Demonstrates:

  • Using a third party library
  • Dealing with Assembly Version Mismatches
  • Using a static Factory method
  • Using instance methods
  • Creating a wrapper class
  • Using a Generic List
  • Wrapping a .NET Collection with FoxPro Collection
  • Checking text spelling and generating spelling suggestsions
  • NuGet Library used: [WeCantSpell.HUnspell]https://github.com/aarondandy/WeCantSpell.Hunspell

Spellchecking is a useful feature if you're dealing with text applications and you can easily integrate a library that can check for misspelled words and suggests correctly spelled words on a misspelling.

Again, I'm not going to provide an integrated solution here, but just the tools to access this functionality. For a complete solution you likely need additional text parsing of larger blocks of text that looks at each word and then somehow highlights each misspelled word and displays a dropdown of suggestions.

Here's an implementation in Westwind Html Help Builder (a FoxPro application using an HTML based Markdown editor interface):

SpellCheck Highlight and Suggest

The editor parses each paragraph for individual words and then feeds each words into the spell checker in FoxPro. If a word is not spelled correctly the editor highlights the word, and on right click requests a list of suggestions that are then displayed in the drop down.

Spell Checking with WeCantSpell.Hunspell

The interface to this library is pretty straight forward:

  • A static factory method initializes a 'word list'
  • The word list is used to check spelling
  • The word list is used to provide a list of suggestions

A good way to start is to look at LinqPad to see what the .NET code looks like which is often cleaner and helps with figuring out what the exact types are:

Spellcheck LinqPad

One thing that jumps out here is that the list of suggestions is returned as a IList<string> result. This is a generic list result which is significant in that generic types are not directly supported in COM or FoxPro and can't be passed back into FoxPro. So we know that the call to the Suggest() method will have to be an indirect call via loBridge.InvokeMethod().

Let's look at the mapped FoxPro code:

loBridge = GetwwDotnetBridge()

loBridge.LoadAssembly("WeCantSpell.Hunspell.dll")

*** Location of Dictionary Files *.dic and *.aff
dictFolder = FULLPATH("bin\")

*** Creates a Spellchecker instance by loading dictionary and grammar files
loSpell = loBridge.InvokeStaticMethod("WeCantSpell.Hunspell.WordList",;
                                    "CreateFromFiles",;
                                    dictFolder +"en_US.dic",;
                                    dictFolder + "en_US.aff")

? "*** Check 'Colour' (en_US)"
? loSpell.Check("Colour")   && false
?
? "*** Check 'Color' (en_US)"
? loSpell.Check("Color")   && true
?

? "*** Suggestions for misspelled 'Colour'"
loSuggestions = loBridge.InvokeMethod(loSpell,"Suggest","Colour")

*** loSuggestions is a  `ComArray` Instance (IList<string>)
? loSuggestions.Count

*** Iterate over array items
FOR lnX = 0 TO loSuggestions.Count -1
   ? loSuggestions.Item(lnX)
ENDFOR

And that works just fine. We can call .Check() directly because it's a very simple method with simple parameters. The .Suggest() method returns a list and in order to access collection values and especially generic list values, we need to use InvokeMethod() to execute the code from within .NET. The result is returned as a ComArray instance, and we can use the .Count and .Item() method to retrieve the individual values.

Wrapper Class Makes it easier

Although the above code is easy enough to use, it's really a good idea to abstract this functionality into a class. You don't want to have to remember how to make the two InvokeMethod() calls and then deal with the ComArray instance. Instead we could create a class that automatically initializes the wordlist and then stores it internally. We can implement a .Spell() method that simply passes through, but for the .Suggest() method we can perhaps turn the ComArray into a native FoxPro Collection.

Here's what that looks like:

*************************************************************
DEFINE CLASS HunspellChecker AS Custom
*************************************************************

oBridge = null
oSpell = null
cLanguage = "en_US"
cDictionaryFolder = "" && root

************************************************************************
FUNCTION init(lcLang, lcDictFolder)
****************************************
IF EMPTY(lcLang)
   lcLang = this.cLanguage
ENDIF
IF EMPTY(lcDictFolder)
   lcDictFolder = this.cDictionaryFolder
ENDIF
   
this.oBridge = GetwwDotnetBridge()
IF ISNULL(this.oBridge)
      ERROR "Failed to load HUnspell: " + this.oBridge.cErrorMsg
ENDIF

LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
if(!loBridge.LoadAssembly("WeCantSpell.Hunspell.dll"))
  ERROR "Failed to load WeCantSpell.Hunspell.dll: " + this.oBridge.cErrorMsg
ENDIF

IF !EMPTY(lcDictFolder)
	lcDictFolder = ADDBS(lcDictFolder)
ELSE
    lcDictFolder = ""
ENDIF

THIS.oSpell = loBridge.InvokeStaticMethod("WeCantSpell.Hunspell.WordList",;
                                    "CreateFromFiles",;
                                  lcDictFolder + lcLang + ".dic",;
                                  lcDictFolder + lcLang + ".aff")

IF ISNULL(this.oSpell)
  ERROR "Failed to load HUnspell: " + this.oBridge.cErrorMsg
ENDIF

ENDFUNC
*   init

************************************************************************
FUNCTION Spell(lcWord)
****************************************
LOCAL llResult

IF ISNULLOREMPTY(lcWord) OR LEN(lcWord) = 1
   RETURN .T.
ENDIF

llResult = this.oSpell.Check(lcWord)

RETURN llResult
ENDFUNC
*   Spell

************************************************************************
FUNCTION Suggest(lcWord)
****************************************
LOCAL loWords, lnX

loCol = CREATEOBJECT("collection")

*** Returns a Collection of values (not an array)
loWords = this.oBridge.InvokeMethod(this.oSpell,"Suggest",lcWord)

lnCount = loWords.Count

FOR lnX = 0 TO lnCount -1
    *** return indexed value (0 based) from the list collection
    lcWord = loWords.Item(lnX)
    loCol.Add( lcWord )
ENDFOR


RETURN loCol
ENDFUNC
*   Suggest

************************************************************************
FUNCTION Destroy()
****************************************

*** MUST dispose to release memory for spell checker
*this.oSpell.Dispose()
this.oSpell = null

ENDFUNC
*   Destroy

ENDDEFINE

This makes it much easier to create an instances of the class and keep it around for the duration of the application. You'll want to minimize loading this class as loading the dictionary from disk initially can be on the slow side (takes a second or two).

Beyond that the Suggest() method returns a cleaner FoxPro collection that you can FOR EACH over and use with more familiar 1 based array logic.

Using this class the previous code gets a little simpler yet:

*** Using the Wrapper Class
CLEAR

*** Loads library and dictionary - you can cache this for reuse
loSpell = CREATEOBJECT("HunspellChecker","en_US",".\bin")   


? "*** Check spelling for:"
? "Testing: " + TRANSFORM( loSpell.Spell("Testing")  )

? "Tesdting: " + TRANSFORM(loSpell.Spell("Tesdting")      )


lcWord = "aren'tt"
? "Suggestions for " + lcWord
loSug = loSpell.Suggest(lcWord)
? loSug.Count
FOR EACH lcWord in loSug
  ? lcWord
ENDFOR
loSpell = null

loSpell = CREATEOBJECT("HunspellChecker","de_DE",".\bin")

? "*** Using German Dictionary:"
? "Zahn: " + TRANSFORM(loSpell.Spell("Zahn"))
? "Zahnn: " + TRANSFORM(loSpell.Spell("Zahnn"))     
? "Zähne: " + TRANSFORM(loSpell.Spell("Zähne"))  
? "läuft: " + TRANSFORM(loSpell.Spell("läuft"))

? "***  Suggest for Zähjne"
loSug = loSpell.Suggest("Zähjne")
FOR EACH lcWord in loSug
  ? lcWord
ENDFOR
     
? loSug.Count
loSpell = null


? "*** Text to check:"
TEXT TO lcText
These ae somme of the worsd that are badly mispeled.

I cannot imaggine that somebody can spel so bad.

ENDTEXT

Note that you can spell check for different languages by applying the appropriate dictionary files. HUnspell uses standard dictionary files that are also used by OpenOffice and you can download other languages from there.

Finally, here's a rudimentary implementation that lets you get all misspelled words in a paragraph or other lengthy block of text and then also return the suggestions for the misspelled words:

loSpell = CREATEOBJECT("HunspellChecker","en_US",".\bin")   
loWords = GetWords(lcText)

LOCAL lnX
? "*** Mispelled words:"
FOR lnX = 1 TO loWords.Count   
   lcWord = loWords.Item(lnX)
   lcSuggest = ""

   IF (!loSpell.Spell(lcWord))
      loSug = loSpell.Suggest(lcWord)
      IF (loSug.Count > 0)
      	  
          FOR lnY = 1 TO loSug.Count
			lcSuggest = lcSuggest + loSug.Item(lnY) + " "
	   	  ENDFOR
	  ENDIF
	  
	  ? lcWord + " - " + lcSuggest   
	ENDIF
	
ENDFOR

From here you can build an interactive solution to integrate spellchecking into your own applications.

A Library with Dependency Issues

It turns out this library is distributes as a .NET Standard library which means that it's not specifically targeted at .NET Framework (ie. 4.72 or later) but rather uses a more generic target. It also takes advantage of some newer .NET features that are not part of the core framework and require additional dependencies.

Spellcheck Libraries

As you can see there are several extra libraries that have to be distributed. That's easy enough, however it turns out that other applications also have dependencies on these same libraries and require slightly different versions.

When this happens it's necessary to set up the appropriate assembly redirects that roll up to the latest version of each library. To do this you have to edit the following files:

  • Yourapp.exe.config
  • vfp9.exe.config (in the VFP9 install folder)
<?xml version="1.0"?>
<configuration>
  <startup>   
	<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />	
  </startup>
  <runtime>
    <loadFromRemoteSources enabled="true"/>
      
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
            <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
            </dependentAssembly>
            <dependentAssembly>
            <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
            </dependentAssembly>
            <dependentAssembly>
            <assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.1.2" />
            </dependentAssembly>
            <dependentAssembly>
            <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.3.0" />
       </dependentAssembly>
     </assemblyBinding>     
     
  </runtime>
  
</configuration>  

It's important to note that the versions in the assembly bindings are Assembly Versionsnot File Versions. You can find assembly versions in tools like ILSpy by looking at each assembly and looking at the metadata for the assembly.

IL Spy Version Number

Humanize numbers, dates, measurements

Demonstrates:

  • Using a third party library
  • Creating a Wrapper Class in .NET to avoid type complexity
  • Create a Wrapper class in FoxPro for ease of use
  • Work with Extensions Methods in .NET
  • Nuget Library: Humanizer

In this example, we'll see an example of creating a custom .NET DLL of our own and calling it from FoxPro. Humanizer is a cool library for text formatting that - as the name suggests - provides friendly naming for many common types. For example, things like using Today or 10 days ago for dates, turning numbers into strings, pluralizing text, camel or snake casing un-casing of text, turning bytes into friendly descriptions and so on.

Examples of Humanizing Number, Dates, Words, Sizes, and more

What's different with this example and why I prefer to delegate access of features to a .NET wrapper class is that Humanizer implements most of its functionality as extension methods and it uses a lot of nullable types. It's possible to access these features through wwDotnetBridge, but it's just significantly easier to do from within .NET and it's possible to create a very simple .NET class that exposes the features we want directly to FoxPro without the need of a FoxPro wrapper class.

To demonstrate some of the features of Humanizer and the functions I've exposed here's an example of the FoxPro code that uses the .NET wrapper component first:

loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly("wwDotnetBridgeDemos.dll")

*!*	? "*** Raw Date to String"
*!*	*  public static string Humanize(this DateTime input, bool? utcDate = null, DateTime? dateToCompareAgainst = null, CultureInfo culture = null)      
*!*	ldDate = DATE() + 22
*!*	? loBridge.InvokeStaticMethod("Humanizer.DateHumanizeExtensions","Humanize",ldDate,null,null,null)  
?

*!*	? "*** Raw Number to String"
*!*	lnValue = 121233
*!*	* public static string ToWords(this int number, CultureInfo culture = null)       
*!*	? loBridge.InvokeStaticMethod("Humanizer.NumberToWordsExtension","ToWords",lnValue, null) + ;
*!*	"  (" + TRANSFORM(lnValue,"9,999,999") + ")"
*!*	?

*** Using a .NET Wrapper Class

LOCAL loHuman as wwDotnetBridge.FoxHumanizer
loHuman = loBridge.CreateInstance("wwDotnetBridgeDemos.FoxHumanizer")

? "*** Human Friendly Dates"
? loHuman.HumanizeDate(DATE()-1)            && yesterday
? loHuman.HumanizeDate(DATETime() + 86500)  && tomorrow
? loHuman.HumanizeDate(DATE() + 2)          && 2 days from now
? loHuman.HumanizeDate(DATETIME() - 55)     && 55 seconds ago
? loHuman.HumanizeDate(DATETIME() - 3800)   && an hour ago
?

? "*** Number to Words"
? loHuman.NumberToWords(10)        && ten 
? loHuman.NumberToWords(1394)      && one thousand three hundred ninety four
?
? "*** Pluralize"
? loHuman.Pluralize("Building")    && Buildings
? loHUman.Pluralize("Mouse")       && Mice
?

? "*** Numbers and Pluraize together"
? loHuman.ToQuantity("Car",3)       && three cars
? loHuman.ToQuantity("Mouse",3)     && three mice
?

? "*** Bytes, kb, megabytes etc. from bytes"
? loHuman.ToByteSize(13122)         && 12.81 KB
? loHuman.ToByteSize(1221221)       && 1.16 MB           
? loHuman.ToByteSize(1221221312)    && 1.14 GB

Humanizer has a ton of features and I've just exposed a few of them in the .NET class. Here's the C# class code:

/// <summary>
 /// Helper to create humanized words of numbers dates and other occasions
 /// 
 /// Wrapper around hte Humanizer library:
 /// https://github.com/Humanizr/Humanizer
 /// </summary>
 public class FoxHumanizer
 {

     /// <summary>
     /// Humanizes a date as yesterday, two days ago, a year ago, next month etc.
     /// </summary>
     public string HumanizeDate(DateTime date)
     {            
         return date.Humanize(utcDate: false);
     }

     /// <summary>
     /// Turns integer numbers to words
     /// </summary>
     public string NumberToWords(int number)
     {            
         return number.ToWords();
     }

     /// <summary>
     /// Returns a number like 1st, 2nd, 3rd
     /// </summary>
     public string NumberToOrdinal(int number)
     {
         return number.Ordinalize();
     }

     public string NumberToOrdinalWords(int number)
     {
         return number.ToOrdinalWords();
     }

     /// <summary>
     /// creates expression like one car or two bananas
     /// from a qty and a string that is pluralized as needed
     /// </summary>
     public string ToQuantity(string single, int qty)
     {
         return single.ToQuantity(qty, ShowQuantityAs.Words);
     }


     public string ToCamelCase(string input)
     {
         return input.Camelize();
     }

     /// <summary>
     /// Truncates a string and adds elipses after length is exceeded
     /// </summary>
     public string TruncateString(string input, int length)
     {
         return input.Truncate(length);
     }

     /// <summary>
     /// Takes a singular noun and pluralizes it
     /// </summary>
     public string Pluralize(string single)
     {
         return single.Pluralize(true);
     }

     /// <summary>
     /// Takes a pluralized noun and turns it to singular
     /// </summary>
     public string Singularize(string pluralized)
     {
         return pluralized.Singularize(true);
     }

     /// <summary>
     /// Returns a byte count as kilobytes, Mb or Gb
     /// </summary>
     public string ToByteSize(int byteSize)
     {
         return byteSize.Bytes().Humanize("#.##"); 
     }
     
 }

Humanizer has a ton of features, so I picked specific features that I am interested in to expose in the FoxPro class. If there are other use cases that I have not figured out here I can add them separately.

If I want to update the code I can simply update the code and rebuild the wwDotnetBridgeDemos.csproj project which lives in its own folder in .\Dotnet\wwDotnetBridgeDemos.

To update the code and assuming I have the .NET SDK installed on my machine I can simply run the following from a terminal in that folder:

dotnet build wwDotnetBridgedemos.csproj

The project is set up to automatically build into the .\bin folder but make sure you've shut down FoxPro if the library was previously loaded.

Why use a .NET Class?

So why did I use a .NET class if it's possible to call the code from FoxPro? In some cases it's simply easier to use .NET code to call functionality in .NET. Humanizer in particular uses a couple of features that are a little tricky to access from FoxPro.

Consider this LinqPad code:

Humanizer LinqPad

Most of Humanizer's methods are heavily overloaded extension methods that use nullable types all of which are not super easy to call from FoxPro using wwDotnetBridge. It's possible, but it's super time consuming to find the right overload to call and then set up the ComValue structure correctly to support the nullable values.

It's simply easier and also considerably more efficient to use a .NET wrapper directly for these calls especially since our own classes can be designed to be explicitly FoxPro friendly so they can be directly invoked without any of the wwDotnetBridge proxy helper methods.

A File System Watcher and Live Reload (Event Handling)

Demonstrates:

  • Using built-in .NET functionality
  • Handling events via a Callback class
  • Useful for document change notifications
  • Useful for a Live Reload Manager
  • Uses System.Net.FileSystemWatcher

The .NET FileSystemWatcher is a very useful built-in class that can be used to monitor changes in the file system. You can enable the file watcher to notify you when files change, are added, deleted or renamed. There are many uses for this functionality especially in document centric applications.

Real World Examples of Monitoring Files

For example, I use this functionality in Markdown Monster for example to detect when a document I'm editing has changed. If my document has no changes I can automatically update the document with the new changes from disk, or if there are changes I can pop up a notice when saving that the file has been changed since opening. At that point I can pop up a dialog letting me choose between my copy, the changed copy, or do text merge in a Comparison Tool (BeyondCompare for me). Here's what this looks like:

Document File Change Detection

The way this works is that the File Watcher is created when the document is opened and I check for any changes for that specific file only. Then based on current document state the code executes an update or sets a flag that triggers the dialog on save.

Another use case for this is in Web Connection for the Live Reloading the server if FoxPro code has changed. I can basically monitor for any code changes to PRG files, and if one has been change I can automatically restart the Web server application and run with the new change and at the same time trigger a page refresh in the browser to reload the page. Meanwhile the Web Connection Web connector which also uses a FileSystemWatcher monitors for changes to Web files and automatically triggers a browser refresh when HTML, CSS or JS files change.

It's a powerful feature and there are lots of use cases for it.

Using Async Callbacks to get File System Notifications in FoxPro

The FileSystemWatcher is a tricky component to work with, as it doesn't allow for filtering, so you basically monitor changes to all files and then handle filtering as part of your event handling code. The component uses events which is something new to discuss in regards of FoxPro and wwDotnetBridge access to this component.

Let's start with the setup code that starts the file monitoring:

loBridge = GetwwDotnetBridge()

IF EMPTY(lcFileList)
   lcFileList = "*.prg,*.ini,*.fpw,*.bak,"
ENDIF
IF EMPTY(lcFolder)
   lcFolder = SYS(5) + CurDir()
ENDIF

*** Create an instance of the file watcher: MUST PERSIST!!!
PUBLIC __WWC_FILEWATCHER
__WWC_FILEWATCHER = loBridge.CreateInstance("System.IO.FileSystemWatcher",lcFolder)
__WWC_FILEWATCHER.EnableRaisingEvents = .T.
__WWC_FILEWATCHER.IncludeSubDirectories = .T.

*** Create the Handler Object that responds to events
loEventHandler = CREATEOBJECT("FileWatcherEventHandler")
loEventHandler.cFileList = lcFileList

*** Create a subscription to the events
loSubscription = loBridge.SubscribeToEvents(__WWC_FILEWATCHER, loEventHandler)
loEventHandler.oSubscription = loSubscription  && so we can unsubscribe

? "Watching for file changes in: " + lcFolder + " for  " + lcFileList

The code creates a new FileSystemWatcher instance and enables listening to events. We specify a folder to monitor recursively and allow raising of events. The watcher exposes a number of events and we need to implement all events of the interface even if we are only interested in a few or even one event. This is similar to the way COM events are handled via FoxPro INTERFACE.

Next we create an Event Subscriptiong by calling SubscribeToEvents() to which we pass the object that we want to handle events on and an event handler object - that's the object that implements the methods matching the event signatures in .NET.

The events are fired asynchronously, meaning the event handler methods are fired out of band and the code immediately following the SubscribeToEvents() immediately returns and keeps on running. The events fire in the background, out of band.

The event handler class is then called when something interesting happens - in this case when files are changed in some way. For this example, I'm simply writing the operation and filename to the desktop, but you can do anything you want from within the event code.

You do however, want to minimize the code you run in the event handler and have it touch as little shared data as possible. It's best to consider event handlers as data drops where you dump some data that can be picked up by the main application at a later time. This can be as simple as setting one or more variables that are later read, or writing a record to a database table that is later read.

Here's the EventHandler class:

*************************************************************
DEFINE CLASS FileWatcherEventHandler AS Custom
*************************************************************

*** File extensions to monitor
cFileList = "*.prg,*.vcx,*.exe,*.app,*.ini"

*** Optional comma list of folders to exclude (ie: \temp\,\web\)
cFolderExclusions = ""

*** File watcher subscription
oSubscription = null

nLastAccess = 0

************************************************************************
FUNCTION HasFileChanged(lcFilename as String)
****************************************
LOCAL lcFileList, lnX, lnFiles, lcExtension, lnExclusions
LOCAL ARRAY laExtensions[1]
LOCAL ARRAY laExclusions[1]

IF EMPTY(lcFileName)
   RETURN .F.
ENDIf


IF ATC("\temp\",lcFileName) > 0
   RETURN .F.
ENDIF

lnExclusions = 0
IF !EMPTY(THIS.cFolderExclusions)
	lnExclusions = ALINES(laExclusions,this.cFolderExclusions,1 + 4, ",")
	FOR lnX = 1 TO lnExclusions
	    IF ATC(LOWER(laExclusions[lnX]),lcFileName) > 0
	       RETURN .F.
	    ENDIF
	ENDFOR
ENDIF

lcFileList = STRTRAN(THIS.cFileList,"*.","")
lnFiles =  ALINES(laExtensions,lcFileList,1 + 4,",")
        
FOR lnX = 1 TO lnFiles
    lcExtension = LOWER(JUSTEXT(lcFileName))
    IF lcExtension == LOWER(laExtensions[lnX])
       this.nLastAccess = SECONDS()
       RETURN .T.
    ENDIF
ENDFOR

RETURN .F.
ENDFUNC
*   HasFileChanged


************************************************************************
FUNCTION OnCreated(sender,ev)
****************************************
LOCAL lcFile 

lcFile = THIS.GetFilename(ev)
IF THIS.HasFileChanged(lcFile)
   ? "File has been created: " +lcFile
ENDIF
	
ENDFUNC

FUNCTION OnChanged(sender,ev)
LOCAL lcFile 

lcFile = THIS.GetFilename(ev)

IF THIS.HasFileChanged(lcFile)
	? "File has been changed: " + lcFile
ENDIF

ENDFUNC

************************************************************************
FUNCTION OnDeleted(sender, ev)
******************************
LOCAL lcFile 

lcFile = THIS.GetFilename(ev)
IF THIS.HasFileChanged(lcFile)
	? "File has been deleted: " + lcFile
ENDIF

ENDFUNC

************************************************************************
FUNCTION OnRenamed(sender, ev)
******************************
LOCAL lcFile 

IF VARTYPE(ev) # "O"
   RETURN
ENDIF

*** RenamedEventArgs apparently doesn't allow direct access
loBridge = GetwwDotnetBridge()
lcOldFile = loBridge.GetProperty(ev,"OldFullPath")
IF EMPTY(lcOldFile)
   RETURN
ENDIF
lcNewFile = loBridge.GetProperty(ev,"FullPath")
IF EMPTY(lcNewFile)
   RETURN
ENDIF

? "File has been renamed: " + lcOldFile + " to " + lcNewFile

ENDFUNC

************************************************************************
FUNCTION Destroy()
******************

IF THIS.oSubscription != null
	THIS.oSubscription.UnSubscribe()
	THIS.oSubscription = null
ENDIF
   
IF VARTYPE(__WWC_FILEWATCHER) = "O"
   __WWC_FILEWATCHER.Dispose()
   __WWC_FILEWATCHER = .F.
ENDIF

ENDFUNC

ENDDEFINE

The file watcher has no file matching filters that can be applied to the events which means that you have to filter for files that you want to watch as part of the event handlers you implement. If you're only interested in PRG files you need to check what files are incoming and immediately exit if it's not what you want.

Another tricky part about the file watcher is knowing exactly what the event interface looks like which in this case can be found here.

As mentioned all events on the event interface have to be implemented, but you can certainly provide do nothing event handling the simple exit immediately. If you don't care about renaming, just immediately return in the OnRenamed event.

When you run this demo you might also notice that some events like the change events fire multiple times. That's because there are filters that you can set on what can be monitored and many file operation might trigger for multiple matches to these triggers. It might be file data change and data change. Making the file watcher behave often involves ignoring repeated events and playing around with the event filters.

Regardless of the minor issues, a Filewatcher is a powerful tool that's useful for a ton of features.

Async: Print Html to Pdf

Demonstrates:

  • Calling async Task methods
  • Using Callback handlers to handle async completions
  • Find out how to turn HTML to PDF
  • NuGet Library: Westwind.WebView.HtmlToPdf

This is a small example that demonstrates how to print HTML documents to PDF files. This is useful for Web and desktop applications that can output reports and documents to HTML and turn that HTML into PDF files that can be emailed or published.

This and the following example require the use of async Task code in .NET as the task of printing to PDF can take a bit of time. So rather than waiting for completion the operation is async and calls you back when the task is complete.

Introducing InvokeTaskMethodAsync()

wwDotnetBridge includes a InvokeTaskMethodAsync() which calls an async method and responds back via a Callback object that you pass in. This is similar to the way the FoxPro EVENTHANDLER() function works except that we're passing the callback and event method through to .NET rather than to a COM object.

The AsyncCallbackEvents handler object that is passed to the method is notified when the async operation completes either via a OnComplete() or OnError() handler. Unlike the event interface we discussed in the last example, here we only have to implement two methods and they always have the same signature, although OnComplete() will receive a different result each time, depending on a successful return value from the async operation.

Printing PDF Output from HTML Input

Let's take a look by way of an example that uses a third party (mine) .NET component that converts PDF documents to HTML using the Edge WebView runtime in Windows using the Westwind.WebView.HtmlToPdf component.

Let's start with the mainline setup code that starts the Html to Pdf operation.

LPARAMETER lcUrl, lcOutputFile

*** Has to be persist after program ends 
PUBLIC loCallbacks

IF EMPTY(lcUrl)
   lcUrl = "Assets/HtmlSampleFile-SelfContained.html"
ENDIF
IF EMPTY(lcOutputFile)
   lcOutputFile = "c:\temp\htmltopdf.pdf"
ENDIF   
IF !StartsWith(lcUrl, "http")  
   *** Assume it's a path - fix it up
   lcUrl = GetFullPath(lcUrl)
ENDIF

CLEAR 
? "*** Generating PDF from " + lcUrl
? "*** To " + lcOutputFile

LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly("Westwind.WebView.HtmlToPdf.dll")

*** Create the .NET class
loPdf = loBridge.CreateInstance("Westwind.WebView.HtmlToPdf.HtmlToPdfHost")

*** Create the Callback Handler Object
loCallbacks = CREATEOBJECT("PdfCallbacks")
loCallbacks.cOutputFile = lcOutputFile
ERASE (lcOutputFile)

loSettings = null
loSettings = loBridge.CreateInstance("Westwind.WebView.HtmlToPdf.WebViewPrintSettings")

*** Async PDF Generation Method: Exits immediately
loBridge.InvokeTaskmethodAsync(loCallbacks, loPdf,
                               "PrintToPdfAsync", lcUrl, 
                               lcOutputFile, loSettings)

? "*** Converting to PDF - this may take a few seconds..."
GoUrl(lcUrl)   && Display PDF

The base behavior for this class is very simple - you call a single method to perform the Html to PDF conversion. You basically provide a Url or Filename to an HTML document, and an optional output filename. The engine then renders the HTML in the WebView instance and uses the WebView's built-in PDF engine - the same engine that's used for printing and PDF printing - and renders the active HTML document as best as it can to PDF.

PDF Output Generation isn't an exact Science

PDF output is basically Print output, so the PDF output generated will be the same as what you see in Print Preview window of any Chromium browser. So if you want to get an idea how well PDF printing works for a specific document/url you can test the output before you ever try to print it.

Print output of HTML can be effected by HTML styling - not everything that renders well as HTML prints well. Additionally it's very useful to add print specific CSS styling that simplifies HTML rendering, uses specific built-in fonts etc. to optimize printing. When it comes to print output simpler is better - and plain semantic HTML (ie. document centric output) works best.

The call PrintToPdfAsync() is async so it returns immediately. You pass in a Callback handler object that is called back when the print operation completes - or fails. You also pass the Url, output file and a settings object which allows customizing a few things about the PDF output generation.

Here's what the Callback object looks like:

DEFINE CLASS PdfCallbacks as AsyncCallbackEvents

*** Capture output file 
cOutputFile = ""

FUNCTION OnCompleted(lvResult,lcMethod)

IF !lvResult.IsSuccess
   ? "ERROR: " + lvResult.Message
   RETURN
ENDIF

GoUrl(THIS.cOutputFile)
? "*** PDF Output Generated!"

ENDFUNC

FUNCTION OnError(lcMessage, loException, lcMethod)

? "Error: " + lcMethod,lcMessage

ENDFUNC

ENDDEFINE

You inherit from AsyncCallbackEvents and implement two methods:

  • OnCompleted(lvResult, lcMethod)
  • OnError(lcMessage, loException, lcMethod)

The reason for inheriting is that in some cases you may not need to handle either of these methods - in fire and forget scenarios for examples. In other cases you don't care about errors and you can omit the OnError handler - the base class can capture and ignore the events. One of these two events will always be called though - and hence the base class to ensure that the methods exist.

In this example, on success we simply go and display the PDF file in the browser via GoUrl(this.cOutputFile). Note that the class adds a custom cOutputFile property that is set when the class is instantiated. Use the property interface to pass values that you need from the mainline into the callback class.

As with the Event code in the previous example, keep event handler methods very short running and have minimal impact on the application state. Ideally capture what you need store it somewhere and then process it from the mainline code.

Here's what the output from the parameterless sample looks like from a local self-contained long HTML document printed to PDF:

Print To Pdf Output

The demo displays both the original HTML and converted PDF document. Here's another example by running to a specific Url:

DO PrintToPdf with "https://microsoft.com"

Here's what the Microsoft Site output looks like (they do a pretty good job with their Print stylesheet):

Print To Pdf Output Microsoft Com

Note that the print results may very in quality, depending how well the URL/HTML support print output via a print specific stylesheet. The library adds some basic print styling but PDF rendering may not do a great job on really complex, and non-semantic HTML documents. It does however very well with very document centric output like documentation or output from - Markdown documents.

Async: OpenAI Calls

Demonstrates:

  • Calling async Task methods
  • Using Callback handlers to handle async completions
  • Use an OpenAI API library to access various OpenAI services online and locally
  • Some useful scenarios for AI
  • NuGet Library: Westwind.Ai

For this example I'll create several different types of AI interfaces that perform specific tasks:

  • Text Summarization
  • Translations
  • Generic Chat Completions
  • Image Generation

OpenAI is a company, but the company was one of the first to expose AI as a service, using the OpenAI HTTP API. This API has become an unofficial open standard and so you find OpenAI style APIs that work with multiple AI engines. In this example, we'll use the HTTP service via front end library that abstracts and wraps the API communications and handles the async calls to the server.

Westwind.AI Library

To do this I'll use one of my own .NET Libraries called Westwind.AI that can interface via HTTP with any OpenAI style API. Using this library you can easily connect to:

  • OpenAI
  • Azure OpenAI
  • Local Ollama Models
  • Any generic OpenAI server

The library is set up in a way that you can easily switch between service providers so you can quickly switch between online models and local models.

The library supports both Chat Completion interfaces as well as image generation via the OpenAI Dall-E 3 model.

Online or Local AIs? LLMs and SLMs - Oh my!

AI comes in all sorts of sizes, but most of you are probably familiar with the large public models or LLMs (Large Language Models) like ChatGPT or CoPilot (Microsoft), Gemini (Google), Llama (Meta) and Grok (X). These are commercial Chat bots that typically run in the browser and communicate with online APIs to provide AI results.

ChatGPT and CoPilot both use OpenAI's models, and they are at the moment typically using GTP-4 or GPT-4o-mini to serve chat requests.

Web based chat bots are cool to play with and useful for assistance tasks, but you can also access these very same models directly from your applications, by using the OpenAI API that is exposed by OpenAI, and Microsoft's Azure services online, or for local machines and many supported local models using Ollama (there are others that also use OpenAI APIs but I haven't tried them since Ollama works well). In other words you can use the APIs to access the power of both online LLMs and offline SLMs (Small Language Models) that can run offline on your own machine.

At the moment online models tend to be much better at providing results, and also much faster than local models. Running local models on non-AI optimized hardware is pretty slow even when running on a reasonably poweful GPU. While many results for say summarizing a 5000 word document via an LLM takes maybe 5 seconds, it can take 30 seconds with a local SLM. The results also tend to be much more... variable with local SLMs. Most of the SLMs I've used for the examples I show here, produce very inconsistent results while the online LLM provide pretty solid results. I suspect this will improve in the future as LLMs are quickly becoming unsustainable for energy and resource usage, and more AI processing eventually will have to local machines. But we're not there yet...

So for best results you'll likely want to use one of the major online LLMs and this library specifically supports OpenAI and Azure OpenAI. Both of these run OpenAI models and if you're using one of these you'll likely use either gpt-4o-mini or gpt-4o. The mini model is smaller, faster and much cheaper version of the full blown gpt-4 models.

Both of these online models are paid services and you pay for API access. OpenAI has a pretty simple credit card signup process and you are charged in relatively small increments. Pricing is quite reasonable even though I use the Image Generation and AI features in Markdown Monster extensively I only replenish my account's $20 balance limit once every two months. Azure Open AI has more complex billing tied to Azure and it uses the same models that OpenAI publishes (with some delay in latest model availability). But it's much more complex to host models as you in effect host your own model service that is tied to a specific model type. Using Azure only makes sense if you have free credits because of a subscription or benefit of some kind, or if you are already heavily using Azure.

Running Local AI Models

Although I mentioned that local models tend to be slower and generally less accurate, it's still very cool that you can run models locally. One big benefit of running local models in a tool like Ollama is that you are not sending your inputs to an online provider so you have privacy. Also the offline models tend to have much more lax rules of what is allowed in AI output generation. With online models it's very easy to run into safety restrictions when running chat or image queries. There are definite free speech restrictions that come into play often in unexpected ways as chat and image queries fixed up by the AI engines. It's much more pronounced with the online services, less so with local AIs although it all depends on how the AI model was tuned.

IAC, for local models I recommend using Ollama which is a locally installable AI Host server. You can download and install it on Windows, and then use command line commands to pull down and run models locally. By default there's a command line interface that you can use, but a better way is to install Open WebUI which lets you run a local browser Chat interface to Ollama. This is similar to ChatGPT or CoPilot, but using local models. Open WebUI also lets you find and download models, and easily switch between multiple installed local models.

Generic Chat Completions from FoxPro

Ok, let's take a look at a few practical examples using Chat Completions. Chat completions is basically the same interface that you use with ChatGPT or CoPilot where you specify a prompt and the AI provides a response. You can provide an Input Prompt which is your request, and also a System Prompt which assigns the AI a Role. The default role of most general purpose AIs is You are a helpful assistant., but you can modify that.

So I start with two practical examples that provide Text Summaries and Language Translation.

Text Summary

So the first example is using chat completions to do text summaries of a block of text. This is a quite useful feature and using the library I use here, relatively easy to do.

Let's start with LinqPad to see what the .NET code looks like:

OpenAi Summary LinqPad

As you can see the key bit of code is the async call:

string result = await chat.Complete(prompt, systemPrompt, false);

Like in the Html to Pdf example we'll need to using InvokeTaskMethodAsync() with a Callback object in FoxPro in order to get asynchronously called back when the relatively slow chat completion returns:

LPARAMETERS lcTextToSummarize
LCOAL loBridge as wwDotNetBridge, loCompletions

loBridge = GetwwDotnetBridge()

*** Using OpenAI API
loConnection = loBridge.CreateInstance("Westwind.AI.Configuration.OpenAiConnection")
loConnection.ApiKey = GETENV("OPENAI_KEY")
loConnection.ModelId = "gpt-4o-mini"  && "gpt-3-turbo" (default so not really neccessary)

IF EMPTY(lcTextToSummarize)
    ? "Please provide some text to summarize."
	RETURN
ENDIF

*** Create Chat client and pass configuration
loCompletions = loBridge.CreateInstance("Westwind.AI.Chat.GenericAiChatClient", loConnection)

*** Our prompt text/question
lcPrompt = lcTextToSummarize

*** Specify a role for the AI
lcSystem = "You are a technical writer and are tasked to summarize input text succinctly " +;
           "in no more than 4 sentences. Return only the result summary text."

*** Callback object that fires OnComplete/OnError events translation is done
loCallback = CREATEOBJECT("OpenAiCallback")
loCallback.oCompletions = loCompletions  

*** Async call starts here - returns immediately
loBridge.InvokeTaskMethodAsync(loCallback, loCompletions,"Complete",lcPrompt, lcSystem, .F.)

? "*** Program completes. Async call continues in background."
? "Summarizing..."

This code is made up of three steps:

  • Setting up the OpenAI Connection
  • Setting up the prompt
  • Calling the OpenAI API

The code's comments describe each operation, as it occurs. The async call returns immediately and there's no success or failure information. Instead the result is handled in the OpenAiCallback class which implements the OnCompleted() and OnError() methods:

DEFINE CLASS OpenAICallback as AsyncCallbackEvents

oCompletions = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lcResult,lcMethod)

IF (this.oCompletions.HasError)
    ? "Error: " + THIS.oCompletions.ErrorMessage
    RETURN
ENDIF

? "Summary:"
? "----------"
? lcResult

ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod,lcMessage
ENDFUNC

ENDDEFINE

To test this out you can copy some text to the clipboard and let it summarize. Just for kicks I took the massive Markdown content of this very white paper and put it in my clipboard. On my machine the result comes back in about 3-4 seconds with a very capable response.

The OnCompleted() method receives the result value from the async call, along with the name of hte method that was called. The latter can be useful if you use the same Callback handler for multiple methods or calls simultaneously and these results might come back at different times.

The value in this case is a text completion, so it's plain text.

OpenAI responses can be returned as text, which in many cases can be markdown. For direct scenarios like the above of text summarization the text is usually returned as plain text as there's nothing to really format. However, for more complex results you will often see Markdown. We'll look at some examples later that demonstrate how to deal with that.

Translations

For the next example lets do essentially the same thing, except this time we'll run translations from one language to another. Essentially the code here is nearly identical to the last example, except that the prompt and system prompt are different along with some of the messages:

LPARAMETERS lcTranslateText, lcLanguage

*** We have to keep the completions alive
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly("Westwind.Ai.dll")

lcOpenAiKey = GETENV("OPENAI_KEY")

loConnection = loBridge.CreateInstance("Westwind.AI.Configuration.OpenAiConnection")

loConnection.ApiKey = lcOpenAiKey
loConnection.ModelId = "gpt-4o-mini"

IF EMPTY(lcTranslateText)
   lcTranslateText = "Genius is one percent inspiration, ninety-nine percent perspiration."
ENDIF
IF EMPTY(lcLanguage)
  lcLanguage = "German"
ENDIF  

loCompletions = loBridge.CreateInstance("Westwind.AI.Chat.GenericAiChatClient", loConnection)
lcSystem = "You are a translator and you translate text from one language to another. " +;
           "Return only the translated text."
lcPrompt = "Translate from English to " + lcLanguage + CHR(13) + CHR(13) + lcTranslateText

*** Set up the callback event handler - OnCompleted/OnError
loCallback = CREATEOBJECT("OpenAiCallback")
loCallback.oCompletions = loCompletions && pass so we can access in callback

*** Make the API call asynchronously - returns immediately
loBridge.InvokeTaskMethodAsync(loCallback, loCompletions,"Complete",lcPrompt, lcSystem, .F.)

? "Translating from English..."
? "--------------"
? lcTranslateText

****************************************************************
DEFINE CLASS OpenAICallback as AsyncCallbackEvents
**************************************************
oCompletions = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lcResult,lcMethod)

IF (this.oCompletions.HasError)
    ? "Error: " + this.oCompletions.ErrorMessage
    RETURN
ENDIF

? "To German:"
? "----------"
? lcResult
ENDFUNC

FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod,lcMessage
ENDFUNC

ENDDEFINE

Again this sample works best with text from the clipboard. Select and copy some text, then translate by doing:

DO openAITranslation with _Cliptext, "German"


DO openAITranslation with _Cliptext, "French"

Cool, right?

Again, if you run these operations multiple times, you'll find that each time it'll translate slightly differently using different words or phrasings. OpenAI does a pretty good job however - I speak fluent German and it usually returns even colloquial German text, even though there's variance.

Using Local LLMs with Ollama

Using the OpenAI online model uses the OpenAIConnection like this:

*** Using OpenAI API
loConnection = loBridge.CreateInstance("Westwind.AI.Configuration.OpenAiConnection")
loConnection.ApiKey = GETENV("OPENAI_KEY")
loConnection.ModelId = "gpt-4o-mini"  && "gpt-3-turbo"

For OpenAI you specify your API key and optionally a model id. In this case the model ID isn't really necessary because currently gpt-4o-mini is the default model.

If I want to run this same example with a local SLM model via Ollama I can do that as well - all I have to do is change the connection. Assuming Ollama is running on my machine, I can change the Connection configuration to:

*** Using Ollama SMLs Locally
loConnection = loBridge.CreateInstance("Westwind.AI.Configuration.OllamaOpenAiConnection")
loConnection.ModelId = "llama3"    && specify a local model (llama3, phi3.5, mistral etc.)

Local Ollama doesn't require an API key so the only thing you have to provide is the local model you want to run. Even that is optional as the default active model is used.

Running against a local model you'll quickly find out that requests are significantly slower and that results are much more variable.

For example, llama3 (Meta's model that has an SLM version) often ignores my request to only return the actual results. For summaries it often refuses to stick to the 4 paragraph limit and gives me a freaking novel instead. phi3.5 (Microsoft's SLM model) often produces bad translations that miss words.

The OpenAI LLMs do a much better job at returning results quickly, and producing more consistently accurate results.

Generic Chat Completions

The last two examples showed how to create specific implementation of chat completions with pre-defined prompts and tasks: Text summarization and translation specifically.

But sometimes it's also useful to just have a generic AI that can provide completely random information. And that's easy to do by simply changing around the way the prompt and system prompt are handled.

Same idea as the last examples, except in this version we can pass in the prompt directly along with an optional system prompt.

LPARAMETERS lcPrompt, lcSystemPrompt

do wwDotNetBridge
DO markdownParser

LOCAL loBridge as wwDotNetBridge, loCompletions
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly("Westwind.Ai.dll")

lcOpenAiKey = GETENV("OPENAI_KEY")

*** Open AI Connection with gpt-4o-mini
loConnection = loBridge.CreateInstance("Westwind.AI.Configuration.OpenAiConnection")
loConnection.ApiKey = lcOpenAiKey

IF EMPTY(lcPrompt)
   lcPrompt = "How do I make an Http call in FoxPro with wwHttp?"
ENDIF
IF EMPTY(lcSystemPrompt)
  lcSystemPrompt = "You are a general purpose, helpful assistant"
ENDIF  

poCompletions = loBridge.CreateInstance("Westwind.AI.Chat.GenericAiChatClient", loConnection)

loCallback = CREATEOBJECT("OpenAiCallback")
loCollback.oCompletions = loCompletions
loBridge.InvokeTaskMethodAsync(loCallback, loCompletions,"Complete",lcPrompt, lcSystemPrompt, .F.)

? "Thinking..."
? "--------------"
? lcPrompt

Nothing new here, except I'm passing in the lcPrompt and optional lcSystemPrompt so we can do:

DO openAICompletions with "Tell me about the history of Southwest Fox"

This time the result will likely be a little different with more information and the interfaces tend to return text as Markdown. Notice at the top I DO MarkdownParser which was covered in one of the earlier samples. We'll use the Markdown parser to parse the result to HTML and then display the result in a templated HTML template so it looks nice.

As before the results are handled in the Async Callback via the OpenAiCallback class:

DEFINE CLASS OpenAICallback as AsyncCallbackEvents

oCompletions = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lcResult,lcMethod)

IF (This.oCompletions.HasError)
    ? "Error: " + THIS.oCompletions.ErrorMessage
    RETURN
ENDIF

*** Convert to Markdown (MarkdownParser.prg)
lcHtml = Markdown(lcResult)

? "Done!"

*** Show Web Page with Formatting
ShowWebPage(lcHtml)

ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod,lcMessage
ENDFUNC

ENDDEFINE

Here's what the result looks like with the result returned in a couple of seconds:

OpenAi Completions SwFoxHistory

While it's cool to see this work and come back with a useful result, think hard about whether you need this functionality built into your app. Essentially this is similar to the type of output you get from ChatGPT or CoPilot which frankly might be better options for users for these types of generic queries. Unless you have very specific use cases like the Translation or Summarizing example I showed it's probably best to avoid having generic AI content embedded in your own apps.

Image Generation

Ok the last AI example is a little different in that it creates image output from an input prompt. OpenAI's Dall-E model allows turning text into images.

Here's an example:

OpenAI ImageGeneration

The results can be pretty cool like the one above, but it might take more than a few tries to arrive at good useful examples. It's also extremely important that you describe your image in great detail, including some directions on what style and coloring the image should use for example.

Without more fanfare here's the code to generate an image:

LPARAMETERS lcPrompt, lcImageFile

do wwDotNetBridge
DO markdownParser

LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly("Westwind.Ai.dll")

lcOpenAiKey = GETENV("OPENAI_KEY")

loConnection = loBridge.CreateInstance("Westwind.AI.Configuration.OpenAiConnection")
loConnection.ApiKey = lcOpenAiKey
loConnection.ModelId = "dall-e-3"
loConnection.OperationMode = 1  && AiOperationModes.ImageGeneration=1

IF EMPTY(lcPrompt)
   lcPrompt = "A Fox that is dressed as a grungy punk rocker, " +;
              "rocking out agressively on an electric guitar. " + ;
              "Use goldenrod colors on a black background in classic poster style format."	 
ENDIF

loPrompt = loBridge.CreateInstance("Westwind.AI.Images.ImagePrompt")
loPrompt.Prompt = lcPrompt

loImageGen = loBridge.CreateInstance("Westwind.AI.Images.OpenAiImageGeneration", loConnection)

loEventHandler = CREATEOBJECT("OpenAICallback")

*** Pass these so they stay alive and can be accessed in the event handler
loEventHandler.oPrompt = loPrompt
loEventHandler.oImageGen = loImageGen

*** Here we need to match the signature EXACTLY which means ACTUAL enum object
enumOutputFormat = loBridge.GetEnumValue("Westind.AI.Images.ImageGenerationOutputFormats","Url")
* enumOutputFormat = loBridge.GetEnumValue("Westind.AI.Images.ImageGenerationOutputFormats","Base64")
loBridge.InvokeTaskMethodAsync(loEventHandler, loImageGen, "Generate", loPrompt, .F., enumOutputFormat) 

? "Generating Image..."
? "--------------"
? lcPrompt

DEFINE CLASS OpenAICallback as AsyncCallbackEvents

oPrompt = null
oImageGen = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(llResult,lcMethod)

IF (!llResult)
    ? "Error: " + this.oImageGen.cErrorMsg
    RETURN
ENDIF

lcUrl = this.oPrompt.FirstImageUrl

? "*** Image URL returned by API:"
? lcUrl

*** Open URL in browser
GoUrl(lcUrl)

*** Download the image
lcImageFile = "d:\temp\imagegen.png"

*** Download the file from URL
LOCAL loBridge 
loBridge = GetwwDotnetBridge()
loWebClient = loBridge.CreateInstance("System.Net.WebClient")
loWebClient.DownloadFile(lcUrl, lcImageFile)

*** Download file with wwHttp if you have it instead
*!*	DO wwhttp
*!*	loHttp = CREATEOBJECT("wwHttp")
*!*	loHttp.Get(lcUrl,  lcImageFile)

GoUrl(lcImageFile)
? "Done!"
ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod,lcMessage
ENDFUNC

ENDDEFINE

ImageGeneration uses an ImagePrompt class that is used to pass in parameters and also retrieve results. You pass in the Prompt property, and when the image generation is complete you get back an array of image image URLs or base64 binary representations of the image. You specify which mode is used via the ImageGenerationsOutputFormats enum which is passed as the last parameter into the Generate() async method:

*** Here we need to match the signature EXACTLY which means ACTUAL enum object
enumOutputFormat = loBridge.GetEnumValue("Westind.AI.Images.ImageGenerationOutputFormats","Url")
* enumOutputFormat = loBridge.GetEnumValue("Westind.AI.Images.ImageGenerationOutputFormats","Base64")
loBridge.InvokeTaskMethodAsync(loEventHandler, loImageGen, "Generate", loPrompt, .F.,enumOutputFormat)

In the example above I returned the URL so the code displays the URL using GoUrl() (from wwUtils.prg) to display the image in the browser. For bonus points the result handler also downloads the image using .NET's System.Net.WebClient and storing it in a file. Alternately you can also use wwHttp if you have it using FoxPro only code.

For returning base64, we can ask for the image to be returned as base64, which is an HTML style image embedding url (data:image/png;base64,<largeBase64Text>) , which can be parsed to binary or by using the convenient loPrompt.SaveImageFromBase64(lcFileToSaveTo) provided by the .NET library. Both Url and base64 work, but base64 is slower as it has to immediately download the image, while URL can just display the URL. URLs are stored only for a limited time on OpenAI, so you'll want to download the URLs if you want to keep any images as soon as you know you want to hang on to them.

There are two programs that demonstrate both URL and base64 downloads:

  • OpenAiImage.prg (Url)
  • OpenAiImageBase64.prg

How do I use this?

The features I've described here I've actually integrated in one of my tools Markdown Monster. Markdown Monster has support for Text Summaries, Translation, Grammar Checking and Image AI generation.

OpenAI MarkdownMonster AIOperations

Here's an example of the AI based Grammar check of a text selection:

OpenAI MarkdownMonster GrammarCheck

Similar specialized dialogs exist for translations, and text summaries that can be embedded or replace existing text.

Image generation is integrated with an interactive editor and image manager that can be used to capture and embed images:

OpenAI MarkdownMonster ImageGeneration

All of these features are implemented using the same library I've shown in the samples above, so these examples are real world integrations that are in production in a commercial application.

AI Summary

Using this AI library makes short work of integrating AI functionality into applications. What I've shown are kind of boring, but practical applications of AI functionality that I've actually used and which is probably a good use of this technology.

Keep in mind that in the current state, AI serves best as an assistant that helps with existing tasks and operations, rather than as a reliable automation tool.

Although there's tons of talk how AI will eat the world, finding more innovative usage for this tech is not easy, because it's really hard to predict exactly what the result from an AI queries look like. As you've seen even with these simple examples, results can vary wildly between runs, even with the same exact data and even with very simple deterministic requests.

So, play around with AI features, but be mindful of how reliable the tools are for the job that you need it to actually perform, before jumping in over your head.

Create a .NET Component and call it from FoxPro

In the course of the examples I've shown there were a couple (wwDotnetBridge 101 and Humanizer) where I demonstrated using a .NET class of our own to create logic. Let's take a look and see how we can actually do that, using the new and much lighter weight .NET SDK tools, that require no specific tools.

The good news is that there's now a .NET SDK that you can download that includes everything you need to compile, build and run .NET applications. It's a single, couple of minutes install and you're ready to start creating .NET code. All you really need is:

  • .NET SDK (download)
  • A text editor
  • A Powershell or Command Terminal Window

You don't need Visual Studio any longer, although if you plan on doing serious .NET development and you want to effectively debug components you build you may still want to use it. Regardless though you can use the SDK and Visual Studio - or any other .NET tools side by side interchangeably.

It's not your Windows .NET anymore

Part of the reason for this shift to more generic tooling is that .NET is no longer a Windows only platform - you can build .NET applications for Mac and Linux. In fact any non-UI and Web applications you build tend to be completely cross-platform out of the box unless you explicitly add platform specific features (ie. Windows API calls, or UI features).

For purposes of this discussion, the goal is to create a .NET Class library that we can call from FoxPro, which means we want to create a new Class Library project.

Creating new Projects

.NET now lets you create new projects from the command line via the dotnet new command.

For FoxPro usage we preferably create projects for .NET Framework - which targets net472 or net481 (4.7.2 recommended since it covers a wider ranger of stock installations).

Unfortunately the SDK does not allow creating .NET Framework projects directly, even though the SDK certainly supports building NET Framework projects. The problem is that newer project types use features that aren't supported for .NET framework, so while you can easily create .NET Core projects and make a few changes, it's a bit of a pain.

The closest you'll get is by targeting netstandard2.0. .NET Standard works with .NET Framework, but ideally you want to target net472 so we'll end up using netstandard2.0 and changing the target.

Let's start by going to a parent folder into which we want to create a new project (for samples, into the dotnet folder to create dotnet\FoxInterop)

Make sure you explicitly specify the target framework and use -f netstandard2.0 as it's the only bare bones project type that's close to what you want for .NET framework.

# go to a parent folder - project is created below
dotnet new classlib -n FoxInterop -f netstandard2.0
cd FoxInterop

# Open an editor in the folder - VS Code here
code .  

Here's what you'll see:

Vs Code Project Netstandard

So you'll want to change the target from netstandard2.0 to net472:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>

</Project>

The class generated is empty.

using System;

namespace FoxInterop
{
    public class Class1
    {

    }
}

And now you're off to the races.

Output to a specific Folder

If you're building for FoxPro you'll likely want your .NET output to go to a specific. It turns out that by default output is created in a complex folder structure below the project.

If you'd rather put the output in your application's output folder (or a bin folder as I like to do for me dependencies) you can change the project file to the following:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>    
    <TargetFramework>net472</TargetFramework>    

    <!-- Optional: Output to a specific folder -->    
    <OutputPath>..\..\bin</OutputPath>
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>    

  </PropertyGroup>
</Project>

In this case I dump the output directly into my bin folder where the samples can find it.

Creating a Sample Class

To create something semi-useful lets create a .NET class and see if we can compile it and access it from FoxPro:

namespace FoxInterop
{
    public class Person
    {
        public string Name { get; set; } =  "Jane Doe";
        public string Company { get; set; } = "Acme Inc.";
        public DateTime Entered { get; set; } = DateTime.UtcNow;
        public Address Address { get; set; } = new Address();

        public override string ToString() 
        {
            return $"{Name} ({Company})\n${Address}";
        }
    }

    public class Address
    {
        public string Street { get; set; }  =  "123 Main St.";
        public string City { get; set; }    =  "Anytown";
        public string State { get; set; }   =  "CA";
        public string PostalCode { get; set; }  =  "12345";

        public override string ToString() 
        {
            return $"{Street}, {City}, {State} {PostalCode}";
        }
    }

}

The Person class has a few simple properties as well as a nested Address object that is automatically initialized. This is good practice to ensure that the Address is never null even if it can be 'empty'.

Build the Project

Next you'll need to actually build the project and you can do this from the Terminal via dotnet build from the project's folder.

Here's what a sucessful build looks like using the built-in terminal in VsCode:

Vs Code Build Project

If the compilation works you'll see a success message, otherwise you'll get an error in the command window along with a line number reference that you can click on:

Vs Code Build Error

Use it in FoxPro

Assuming you get a successful build, the project has built a .NET Assembly (a DLL) into the .\bin folder where it's accessible to our FoxPro code.

Note that there's _startup.prg that adds the .\Bin folder to FoxPro's path and allows .NET assemblies to be loaded out of that folder. All samples call the _startup.prg just in case it's not explicitly set.

So now we can create an instance of our .NET class quite simply. You can type the following into the FoxPro command window, or - better - create a small PRG file to run the code.

CLEAR
DO _startup.prg

DO wwDotnetBRidge
loBridge = GetwwDotnetBridge()
? loBridge.LoadAssembly("foxInterop.dll")
loPerson = loBridge.CreateInstance("FoxInterop.Person")

*** Access the default property values
? loPerson.Name
? loPerson.Entered

*** Call a method
? loPerson.ToString()

You can also access the nested Address object individually:

? loPerson.Address.Street
? loPerson.Address.City
? loPerson.Address.ToString()

Next, in the C# code, let's add a collection of addresses and update the ToString() code to display those addresses if they are present:

public List<Address> AlternateAddresses { get; set; } = new List<Address>();

public override string ToString() 
{
    var output =  $"{Name} ({Company})\n${Address}";

    if (this.AlternateAddresses.Count > 0)
    {
        foreach (var addr in AlternateAddresses)
        {
            output += $"\n${addr}";
        }
    }
    return output;
}

Now rebuild the project from the Terminal:

dotnet build

But - you likely are running into a problem now, namely that the output assembly is locked, because the FoxPro application has it loaded. So you first have to unload FoxPro (or any running application using the assembly).

Then do the build again. Now it works!

Launch FoxPro again and let's add some more code to our test program to add a couple of new alternate addresses.

Let's start by accessing the AlternateAddresses. Notice that the type is List<Address> which is a Generic List object. If you recall, Generics cannot be accessed by FoxPro directly, so we have to indirectly access using GetProperty():

*** Get ComArray of Address
loAddresses = loBridge.GetProperty(loPerson,"AlternateAddresses")

? loAddresses.Count && 0

GetProperty() in this case returns a ComArray object which is an object wrapper around .NET collections. The wrapper allows you to read, add, update and delete items in many different array, list, collection and dictionary types.

In this case we want to add a new item which we can do by using CreateItem() to create a new object of the list's item type, and Add() to add the newly

*** Create a new detail items
loAddress = loAddresses.CreateItem()
loAddress.Street = "3123 Nowhere Lane"
loAddress.City = "Nowhere"

*** Add to the list of addresses
loAddresses.Add(loAddress)

loAddress = loAddresses.CreateItem()
loAddress.Street = "43 Somewhere Lane"
loAddress.City = "Somewhere"

*** Add to the list of addresses
loAddresses.Add(loAddress)

*** Print - should include the new address
? loPerson.ToString()

Using .NET from FoxPro is a good Choice for many Scenarios

And there you have it - it's pretty simple to create a .NET component and call it from FoxPro.

This is quite useful if you need to interact with .NET components that:

  • Require lots of code to interact with
  • Require complex types that are hard to access from FoxPro

As a general rule these days for almost every FoxPro application I create one matching .NET assembly into which I can then easily stuff many small .NET components that can all be called from FoxPro rather than having to figure out how to call various components from FoxPro using wwDotnetBridge indirect access methods. Unless code is super simple, I almost always rather opt for writing in .NET code vs. writing verbose code in FoxPro.

As a bonus this allows you to get your feet wet with .NET code. Using class library extensions like this is a great way to create small bits of .NET code without having to go full bore of a full conversion. You can gain external functionality that otherwise wouldn't be there while still maintaining the ability to easily edit and recompile the code for changes.

Best Practices

.NET Dependency Version Management

One issue that you can run into and that's somewhat important is one of versioning of depdencies. When you use .NET components you may end up using dependencies that are used by multiple components with each version depending on a different version.

.NET - and especially .NET framework - is determinisitic about versions it expects so when a component is bind to a component of a certain type by default it expects that specific version to be present. If it's not you may end up getting assembly load errors, that complain that one or another assembly could not resolve its dependencies.

This situation is better in .NET Core which automatically rolls up dependencies to the highest version, but in .NET Framework you have to do this manually via configuration called Assembly Redirects.

Assembly redirects let you specify a minimum version and desired higher version. By having a redirect all components requesting a specific versions are then 'redirected' to the higher version and so all can share the same version safely.

Assembly redirects are set in .config files. For FoxPro that means:

  • YourApp.exe.config
  • VFP9.exe.config

and here is what Assembly Redirects look like if they are needed:

<?xml version="1.0"?>
<configuration>
  <startup>   
	<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />	
  </startup>
  <runtime>
    <loadFromRemoteSources enabled="true"/>
      
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
            <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
            </dependentAssembly>
            
            <dependentAssembly>
            <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
            </dependentAssembly>
            <dependentAssembly>
            <assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.1.2" />
            </dependentAssembly>
            <dependentAssembly>
            <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.3.0" />
       </dependentAssembly>
     </assemblyBinding>     
  </runtime>
  
</configuration>  

If you're using a single component you're unlikely to run into issues, but for all these demos I'm doing here, there are many different versions of components used and so a conflict is more likely and we're seeing it here.

NewtonSoft.Json is a very widely used JSON parser in .NET so it's often involved in dependency management fix ups.

The various System assemblies are typically caused by components that are using netstandard2.0 instead of explicitly targeting .NET Framework (net472 etc.). If there are multiple components that are using netstandard2.0 it's very likely there will be a version conflict.

So how do you find these problems? When you get an error with a specific assembly you are loading it will typically tell you which dependency is a problem. You can then use a disassembly like ILSpy to check what version is expected and fix the version.

Assembly Versions is what matters for Assembly Redirects

It's important to note that the versions in the assembly bindings are Assembly Versionsnot File Versions. You can find assembly versions in tools like ILSpy by looking at each assembly and looking at the metadata for the assembly. Don't rely on the File Properties window in Explorer which shows the File version.

IL Spy Version Number

Assembly redirects can be a pain to track down, but thankfully they are relatively rare.

Keep it simple: Use .NET When wwDotnetBridge is a lot of Effort

I want to stress once again, to make sure you don't make things too complicated with trying at all costs to keep code purely in FoxPro. Yes, wwDotnetBridge makes it possible to access most .NET code directly, but trust me when I say that it's much easier to create code with proper Intellisense and the ability to directly access the entire language feature set directly rather than having to make indirect calls from FoxPro.

As a bonus by doing this you can isolate the .NET Wrappers in a way so that they are FoxPro friendly so that you can directly access and use the .NET component from FoxPro without a wrapper.

You can create many components in a single .NET project, and .NET assemblies tend to be tiny and very efficient to load. Since you're already going to call into .NET code creating a small wrapper that's FoxPro accessible is going to have virtually no overhead - in fact it'll likely be much more efficient than making indirect calls using wwDotnetBridge.

Take advantage of .NET.

Get your Feet Wet with .NET

Using components is one of the easiest ways to get your feet wet with some .NET code without having to jump into building an entire application. You can slice off some business functionality into .NET or even just those pieces that you can't directly call from FoxPro otherwise.

Many of the success stories of people that have migrated have come from starting very small and getting a feel for it with a few small features and then expanding outwards from there. wwDotnetBridge makes it entirely feasible to build a hybrid application that works both with FoxPro and .NET.

As an example, West Wind Html Help Builder which is a very old FoxPro application integrates heavily with .NET. There are entire sub-components that are written in .NET including the Class and Database Importer, the CHM output generation engine, and large parts of the editor interface. There are even a number of UI Dialogs that are .NET based due to odd behaviors of FoxPro dialogs.

If this sounds like I'm shilling for .NET - in a way I am, but I do so because it has worked extremely well for me. .NET is the easiest platform that you can directly interface with from FoxPro including for desktop applications. There are many other toolsets like NodeJs, Phython, Java etc. that are popular and while I think they are equally as viable as standalone platforms, they don't have anything like .NET in terms of ease of integration with FoxPro or the vast array of Windows system integration. .NET is really the only high level tooling platform that can easily integrate with FoxPro via easy to use COM Interop.

So, take advantage of this easy integration, while gaining an enormous window of functionality that lets you continue to use FoxPro...

Summary

Alright that oughta do it! ??

Let's do a high level sum-up:

  • Lots of cool .NET features available to integrate into FoxPro

    • Many Windows System features are exposed through .NET
    • Thousands of open source and 3rd parties are available
    • With wwDotnetBridge you can access most of it!
  • wwDotnetBridge makes it EASY!

    • Call any .NET components without registration
    • Opens up most of .NET to FoxPro
    • Access most .NET features including
      Generics, Value Types, Collections, Enums, Static members etc.
    • Helpers for types that suck in FoxPro
      ComArray, ComValue
  • Create Wrappers for Complex Code

    • Easier to create .NET code to interface with complex APIs
    • Make the wrapper FoxPro friendly
    • Call the Wrapper from FoxPro
    • Create FoxPro Wrappers for .NET Code

Resources

this white paper was created and published with the Markdown Monster Editor

Making Web Connection Work with Response Output Greater than 16mb

$
0
0

String Limit

During this year's Southwest Fox conference and Scott Rindlisbacher's session, he was discussing generating very large JSON output using West Wind Web Connection with output that exceeds 16mb as part of his application using REST JSON services. The issues raised where two-fold:

  • Output over 16mb would fail
  • Large output was too slow to travel to the client

My first thought was that I had addressed the >16mb issue previously, but after some initial testing it's clear that that was not the case!Web Connection in the past has been limited to 16mb output due to FoxPro's 16mb string limit, for direct Response output. Although there are ways to send larger output via files, that can be a lot more complicated.

During the conference I spent some time to create a solution to this problem and Web Connection can now serve content >16mb. If you're on Web Connection 8.0 and you want to experiment with this updated support, you can pick up an experimental update here:

These features will be shipped with the next release in Web Connection v8.1.

If you're interested, what follows is a discussion on how I worked around this limitation along with a review of how you can create string content that exceeds 16mb in FoxPro in general.

Making >16mb Output work in Web Connection

So while at SW Fox I started playing around with some ideas on how to make >16mb content work, and implemented a solution to this problem.

The solution hinges around intercepting output that is returned and ensuring that no parts of the output strings that are being sent are >16mb, and if they are splitting up those strings into smaller chunks that can be concatenated.

If you want to learn more about how you can use >16mb strings in FoxPro, you can check out my post from a few years ago that shows how this can work if you are careful in how you assign large values.

Working with >16mb Strings

You can assign >16mb by doing something like lcOutput = lcOutput + lcContent, but no part of a string operation that manipulates a string and updates can be larger than 16mb.

The left side of the = can become larger than 16mb, but the right side can never be greater than 16mb.

You also cannot call functions that changes the value of a >16mb string but some functions can return a greater than 16mb string.

Web Connection is already build on incremental building of a string in memory by doing effectively:

FUNCTION Write(lcData)
THIS.cOutput = this.cOutput + lcData
ENDFUNC

which allows writing output >16mb as long lcData is smaller than 16mb. Most of Web Connection's implementation features run through the Response.Write() or friends methods, including complex methods like Response.ExpandScript() and REST Process class JSON output, so as long as no individual Response.Write() operation writes data larger than 16mb, all output automatically can be larger than 16mb.

wwJsonSerializer already supported >16mb JSON output, and with the changes I added this week any response larger than 16mb is chunked into the actual Response output (so there the content effectively gets chunked twice - once for the JSON and once for header/httpoutput).

The issue with the actual HTTP output is that Web Connection pre-pends the HTTP headers in front of the HTTP response. The HTTP response can be larger than 16mb, but if we prepend the headers in front of a 16mb string - that doesn't work (ie. right side of =>16mb).

The workaround for this is: Chunking the string into smaller block that can be written (5mb chunks).

lcHttp = Response.RenderHttpHeader()
lcHttp = lcHttp + Response.GetOutput()  && This fails if the response is >16mb

To fix this I ended up creating a small helper function that splits strings by size to take the string that is larger than 16 megs and splitting it into chunks.

*** Inside of wwProcess::CompleteResponse()
LOCAL lcResponse, lcHttp, lnX, loCol
lcResponse = this.oResponse.Render(.T.)

*** IMPORTANT: Must follow the Render as it may add headers
lcHttp = this.oResponse.RenderHttpHeader()

IF LEN(lcResponse) > 15800000    
   loCol = SplitStringBySize(lcResponse,5000000)  
   FOR lnX = 1 TO loCol.Count
       lcHttp = lcHttp + loCol.Item(lnX)
   ENDFOR
ELSE    	
   lcHttp = lcHttp + lcResponse
ENDIF

IF THIS.oServer.lComObject
  *** Assign text output direct to Server output
  THIS.oServer.cOutput= lcHttp                     && can be >16mb
ELSE
  FILE2VAR(this.oRequest.GetOutputFile(),lcHttp)   && can be >16mb
ENDIF

Every request in Web Connection runs through this method so this single point of output and so any large output can be chunked.

This uses a new helper function called SplitStringBySize():

************************************************************************
*  SplitStringBySize
****************************************
***  Function: Splits a string into a collection based on size
***    Assume: Useful for breaking 16mb strings
***      Pass: lcString  - string to split
***            lnLength  - where to split
***    Return: Collection of strings or empty collection
************************************************************************
FUNCTION SplitStringBySize(lcString, lnLength)
LOCAL lnTotalLength, lnStart, lnEnd, loCollection

lnTotalLength = LEN(lcString)
loCollection = CREATEOBJECT("Collection")

FOR lnStart = 1 TO lnTotalLength STEP lnLength
    lnEnd = MIN(lnStart + lnLength - 1, lnTotalLength)
    loCollection.ADD(SUBSTR(lcString, lnStart, lnEnd - lnStart + 1))
ENDFOR

RETURN loCollection
ENDFUNC
*   SplitString

As it turns out this works for:

  • Plain Response.Write() requests (ie. hand coded)
  • Response.ExpandScript() and Response.ExpandTemplate()
  • Any wwRestProcess handler

Use >16mb Content with Care

Turns out this is a very useful addition, even if I would highly recommend that you don't do this often! It's a bad idea to send huge amounts of data back to the client as it is slow to send/receive, and if you're sending JSON data or HTML table data it'll take forever to parse or render. It also puts a lot of memory pressure on the Visual FoxPro application, and may result in out of memory errors if output is too large and isn't immediately cleaning up.

Reducing Output Size Significantly with GZipCompression

Another thing that you can and should do if you are returning large amounts of text data is enable Response.GZipCompression = .T. either on the active request, or on all requests. Web Connection supports GZip compression of content with just that flag and especially for repeating content like JSON you can cut the size of a document immensely - typical between 5x and 10x smaller than the original size. Using GZipCompression gives you literally a ton more breathing room before you bump up against the 16mb limit.

In the testing I did a bit ago with a huge data set the data went from 18mb down to 2mb with GZipCompression. You get the benefit of a smaller memory footprint, plus vastly reduced transfer time of the data over the wire due to the smaller size.

Response.GZipCompression has a configuration that will only GZip content greater than a minimum size which is configured in wconnect.h (you can override in wconnect_override.h):

#UNDEFINE GZIP_MIN_COMPRESSION_SIZE
#DEFINE GZIP_MIN_COMPRESSION_SIZE				15000

If you plan to universally apply GZip to all content I'd probably bump that to a higher number like 20k before it is likely to have any positive impact vs the compression time.

Summary

16mb for text or even JSON output should be avoid as much as possible. 16mb is a lot of data to either render, or parse as JSON data and I would not recommend doing that under most circumstances. But I know some of you will do it anyway, so this is why we're here ??

So, I've implemented this functionality in the current experimental update for Web Connection v8 so you can play with this right away.

Additionally you can also minimize the need for hitting the 16mb limit in many cases by using Response.GZipCompression = .t. which compresses the Response output. With typical HTML and JSON output, compression is significant with at least a 3x and as much as 10x reduction in output size in many instances. It's a very quick fix to reduce both the output size you're sending and keeping it under 16mb in the first place as well as reducing the network traffic and bandwidth usage significantly.

this post created and published with the Markdown Monster Editor

Web Connection 8.1 Release Post

$
0
0

Hi all,

Web Connection 8.1 has been released. This update is mostly a maintenance release that cleans up a few small bugs and issues, but it also introduces a few notable improvements:

  • Support for greater than 16mb Web Response output
  • More improvements to COM server handling
  • Improved wwDotnetBridge stability (no more crashes after full memory unloads)

Let's take a look.

Support for greater than 16mb Web Response Output

Back in the Web Connection 5.0 days Web Connection switched from file based output to string based output, which it turns out is a lot faster and more flexible than direct file based output. File based output was able to write output of any size as the file content can be written out to any size. String based output on the other hand is - potentially - subject to FoxPro's 16mb memory limit. In version 5.0 Web Connection introduced the wwPageResponse class which switched to an in memory buffer that builds up a string for output.

Essentially it does:

FUNCTION Write(lcText,llNoOutput)

IF !THIS.ResponseEnded
   THIS.cOutput = this.cOutput + lcText
ENDIF  

RETURN ""
ENDFUNC

FoxPro 9.0 is extremely efficient at string concatenation and at the time this provided a large boost in performance - especially related to the complexities of the then introduced Web Connection Web Control Framework (now deprecated) which relied on tons and tons for small components to write small bits of code.

But... with that change came the limitation related to FoxPro's 16mb string limit. Ironically the way Web Connection's response class was designed - unintentionally I might add - the above code for string concatenation actually works just fine even for strings larger than 16mb. Unfortunately, the code would fail later on when writing out the entire response including the headers which required an incompatible string operation.

Specifically it's this code:

*** This fails if cOutput > 16mb
RETURN this.RenderHttpHeader() + THIS.cOutput

This is one of the quirks of the 16mb string limit in FoxPro: You can actually create larger strings, but you cannot concatenate or send a greater than 16mb string to another operation that modifies the string. Essentially that code above in the wwPageResponse::Render() method broke the code.

I wrote in more detail about the workaround to this issue in a previous Blog Post here if you're interested:

The post includes the actual changes in Web Connection along with the code work around and the helpers - you might find those helpers useful in your own code that has to deal with large strings.

The key bit of code that uses the updated string processing is in wwProcess.CompleteResponse():

LOCAL lcResponse, lcHttp, lnX, loCol

*** Grab the response content
lcResponse = this.oResponse.Render(.T.)

*** IMPORTANT: Must follow the Render() as Render() may add headers (GZip/Utf8 etc)
lcHttp = this.oResponse.RenderHttpHeader() 

*** Check for 16mb+ output size
IF LEN(lcResponse) > 15800000    
   loCol = SplitStringBySize(lcResponse,5500000)   
   lcResponse = ""
   FOR lnX = 1 TO loCol.Count
       lcHttp = lcHttp + loCol.Item(lnX)
   ENDFOR
   loCol = null
ELSE    	
   lcHttp = lcHttp + lcResponse
   lcResponse = ""
ENDIF

IF THIS.oServer.lComObject
  *** Assign text output direct to COM Server output
  THIS.oServer.cOutput= lcHttp
ELSE
  FILE2VAR(this.oRequest.GetOutputFile(),lcHttp)
ENDIF
lcHttp = ""

In a nutshell, the code change involves breaking up the large string into a collection of smaller strings, and then building a new string that adds the headers first and then appends the all of the string chunks which now allows for >16mb for both headers and content.

This way there's never a >16mb string on the update side or right side of = assignment operation and so we can write strings of any size, memory permitting (and yes you can run out of memory). It's a hack and ends up costing some extra memory overhead as strings are duplicated, but it works surprisingly well and it now allows us to return very large HTTP responses in memory.

When working with large strings like this, you'll want to clear any in progress strings and the collection as soon as you no longer need it to avoid holding on to memory any longer than you have to.

To be clear Web Connection has had support for more than 16mb output via file operations - using Response.DownloadFile() and Response.TransferFile() which can serve content of any size. But it's a bit more complicated to generate output to file first and then be able to delete the content after it's been sent.
(I'm thinking of another enhancement to allow for file deletion after sending in a future update)

Another related hack for large HTTP text results (HTML, JSON etc.) is to use Response.GzipCompression = .T. which reduces the size of the HTTP output before headers are even added. Especially in the case of JSON compression can often knock down the size of a JSON document by more 5x since there's a lot of repetitive data in JSON or XML documents. There's more info in the above blog post.

There are a couple wwPageResponse class changes that are related to the above changes:

  • A new wwPageResponse::WriteFullResponse(lcData) This method writes out a full response by overwriting any existing data. Added to both wwPageResponse, and wwResponse.

  • wwPageResponse.Render(llOutputOnly)
    The Render() method has a new llOutput parameter that returns only the response content without headers. Render() by default returns both headers and content and this override is used when creating the final output that's sent back to the Web server in file or COM based output.

  • SplitStringBySize() in wwutils.prg
    This splits a string into a collection string chunks. This function is used to split out the >16mb content into smaller strings before being reassembled into a single string including the header.

  • JoinString() in wwutils.prg
    The opposite of the above - takes strings in a collection and returns a single string.

Improvements to COM Server Handling

Web Connection 8.0 has introduced improved COM Server loading, which drastically speeds up COM server loading and provides a lot more checking deterministic termination of servers that cannot be shut down via COM either because they are still busy or have been otherwise orphaned. In the past this has been an ongoing problem for those that were running large numbers of COM Server instances as it took a) very long to load many instances and b) could result in orphaned server instances that would never be shut down - ie. a lot of extra EXEs in task manager.

v8.0 addressed both of these issues with parallel loading of COM servers, immediate server startup while others are still loading and a more robust shutdown sequence that tries to shut down all instances of a given executable as opposed of just the actual process ids of servers that are being shut down.

It took a bit of experimenting during the beta of v8.0 to get this right - many thanks to Richard Kaye during the original beta, and Scott Rindlsbacher during the v8.1 cycle, who kindly both were willing to test with large production and staging environments.

In v8.1 we identified an outlier issue that caused issues with COM server loading for large server pools due to a hard coded timeout that was causing the server pool to not complete loading. This would result in the server load cycle to error out and then try to reload again, going through a rather unpleasant re-cycling loop.

In this update I've made the server load timeout configurable and allow for a larger timeout to begin with. So this is much less likely to run up into the timeout in the first place and if it still occurs can be adjusted with a larger timeout.

The new timeout logic allows for 2 seconds per server to load. That should be plenty, plus parallel loading should reduce the overall time significantly. So if you have 10 servers the load timeout is 20 seconds. If it really takes that long to load servers - there's probably something wrong with the server, as it should not be that slow to load ??

For reference the original release had a max timeout of 5 seconds with 1 second load time per server. I had figured that this would be enough given that parallel loading would ensure that servers load somewhere around maybe 2x their individual load time. But it turns out that if you run in a processor constricted environment that is not uncommon on virtual VPC machines you may still end up loading sequentially. For this reason I removed the max value and stick with the per server load timeout that is configurable.

The new value is in configuration in web.config (Web Connection .NET Module):

<add key="ComServerPerServerLoadTimeoutMs" value="2000" />

Or in WebConnectionServerSettings.xml(.NET Core Web Connection Web Server):

<ComServerPerServerLoadTimeoutMs>2000</ComServerPerServerLoadTimeoutMs>

Improved wwDotnetCoreBridge Stability

In relation to my wwDotnetBridge talk at Southwest Fox I did a bunch of work surrounding wwDotnetBridge.

One issue that's been coming up a few times has been that the new wwDotnetCoreBridge class has been somewhat unstable. In previous releases the library worked fine, but it would fail if FoxPro does a full memory reset via CLEAR ALL or RELEASE ALL etc. What was happening is that unlike the full framework version of wwDotnetBridge, the .NET Core version would clear out all of its memory as the native Windows interface would not pin the loader code in memory.

Thanks to an issue report and some great sleuthing by @horch004 who found the workaround by pinning the loader DLL into memory and thereby ensuring that the memory would not be wiped a FoxPro memory clearing operation.

The fix for the Core is to pin the DLL and then simply return the already loaded .NET wwDotnetBridge instance and that works great - no more crashes after a full memory wipe.

Breaking Change: Updated Log Format

In this release the Web Connection log format has been updated for two things:

  • Widened the RemoteAddr field to support ipV6 addresses
  • Added a Size field to log the response size (if response is captured)

To deal with this you need to update the wwRequestLog FoxPro table or the corresponding SQL table.

For FoxPro

Delete the wwRequestLog.dbf table. It'll be recreated with the new file structure when Web Connection re-starts.

For Sql Server

Update the table using SQL commands or manually in your SQL Admin tool (changed fields separated below):

CREATE TABLE [dbo].[wwrequestlog](
	[time] [datetime] NOT NULL DEFAULT getdate(),
	[reqid] [varchar](25) NOT NULL DEFAULT '',
	[script] [varchar](50) NOT NULL DEFAULT '',
	[querystr] [varchar](1024) NOT NULL DEFAULT '',
	[verb] [varchar](10) NOT NULL DEFAULT '',
	[duration] [numeric](7, 3) NOT NULL DEFAULT 0,
	
	[remoteaddr] [varchar](40) NOT NULL DEFAULT '',
	
	[memused] [varchar](15) NOT NULL DEFAULT '',
	[error] [bit] NOT NULL DEFAULT 0,
	[reqdata] [text] NOT NULL DEFAULT '',
	[servervars] [text] NOT NULL DEFAULT '',
	[browser] [varchar](255) NOT NULL DEFAULT '',
	[referrer] [varchar](255) NOT NULL DEFAULT '',
	[result] [text] NOT NULL DEFAULT '',
	
	[size] [int] NOT NULL DEFAULT 0,
	
	[account] [varchar](50) NOT NULL DEFAULT ''
) ON [PRIMARY]

Summary

There you have it. Web Connection 8.1 is a minor update and other than the logging table changes for SQL there are no breaking changes. As always you can update the project by checking out the update topic in the documentation.

If you run into any issues please post a message on the message board:

this post created and published with the Markdown Monster Editor

FoxPro Running on a Windows ARM Device

$
0
0

I recently picked up a Windows ARM device in the form of a Samsung Galaxy Book 4 with a SnapDragon X Elite chip. Best Buy had a sale going for $799 at the time, and so I 'snapped' one up.

I was pleasantly surprised that almost all of my Windows applications run without issues. This is true for both .NET and FoxPro applications as far as my own apps are concerned, and just about anything else in my typical Windows application and tools collection.

FoxPro and Web Connection

One of the things I was really curious about was whether FoxPro applications would work. After all FoxPro is a 32 bit application (ie. not x64) and it's based on 30 year old Windows code.

In fact, to test out FoxPro operation under ARM I tried the following from scratch:

  • Install Visual FoxPro and SP2-HF3
  • Copy over my Web Connection Dev folder from my main laptop

Then proceeded to fire up FoxPro in my dev folder and ran:

Launch()

which in my setup launches the Web Connection .NET Core Web Server.

To my delight, everything fired right up and ran on the very first try - without any changes whatsoever:

Full disclosure: I did already have the .NET 8.0 SDK installed in order for the .NET Core server to run

The Web Connection FoxPro server runs fine under ARM in x64 emulation mode, but surprisingly performance is very good. Since this is a Windows Home machine full IIS is not an option, but I was able to set up IIS Express on this machine and it too just works on the first try:

Iis Express Running On Arm

IIS Express runs in x64 emulation but that's also not a problem and performance seems to be on par with the .NET Core version. That performance of emulation is almost on par with the native .NET Core server's ARM binary is almost more impressive than the .NET server running without any changes at all.

Neither the native .NET Core server or the Web Connection Module under IIS Express feel laggy and request hits are in a the 5-10ms range which is a only little slower than what I see on my i9 development laptop (trending towards the higher end of the range on ARM vs. lower end on I9).

Earlier also fired up West Wind Html Help Builder, which is a FoxPro desktop application running with a FoxPro runtime installation and it also worked without any issues. That app does a lot of oddball things with FoxPro UI, ActiveX controls, the WebBrowser control and native Win32 calls. Yet it just runs without any issues.

Pretty cool!

Everything works - except SQL Server!

When I picked this machine up and started installing my typical developer and productivity workload on it, I was expecting a lot of stuff to not work or work badly through emulation. It turns out I was wrong! Mostly!

Just about everything works!

I've been pleasantly surprised in that just about everything works - either via many native ARM applications that are now becoming available, or via the built in and 'not bad at all' x64 emulation.

There are a few things that don't work well - older versions of SnagIt for example don't want to capture system windows correctly and my audio software was having problems keeping the external audio interface connected. Mostly edge cases, but be aware there are somethings - especially hardware or low level windows stuff - that can have problems.

The one let down is that performance is not quite as impressive as was advertised hyped. It feels like there's quite a bit of lag, especially during application startup initially. Performance overall feels at best like a mid-level laptop, certainly not something that would replace a higher end developer I7/I9 laptop for me, which I had expected given the promotional hype. Even local AI operations using Ollama which is what these machines were supposed to be best at are only barely better for local AI processing compared to my I9 and low-midrange nVidia card equipped laptop do.

.NET Applications - Just Run Natively!

What is quite impressive though is that all of my .NET apps, - including a several complex Windows Desktop applications - ran without even recompiling, natively under ARM64. Markdown Monster and West Wind WebSurge both run without any changes and I have yet to find any problems on ARM. All of my Web apps also 'just run' under ARM64 and I have yet to see any errors or operational differences. That's impressive - especially for the desktop apps which use a ton of P/Invoke and native code in addition to raw .NET code. And it... just... works!

The one thing that was problematic: SQL Server

The only real issue I ran into was SQL Server, which does not have an installer from Microsoft that works with Windows ARM.

There are two workarounds that I discuss in a separate post on my main blog:

  • Using LocalDb with Named Pipes
  • Using a hacked installer to install SQL Express or SQL Developer

If you need to work with a local copy of SQL Server check out the post that goes into greater detail.

Summary

So, the good news is ARM devices have a lot going for it. There are now many native ARM applications available - most mainstream applications have ARM specific versions or can run under ARM. Most .NET applications can automatically run natively on ARM if they are compiled for AnyCPU mode.

For those apps that don't have native support x64 Emulation is surprisingly good and fairly fast. Frankly I'm not sure that I can really tell the difference from emulation to native - emulated apps seem a little slow to start up, but once running the emulation seems as fast as I would expect for this machine's processing power (ie. a mid-level business laptop).

In summary ARM device compatibility is much better than I expected and performance is about on par what I thought it would be, but a bit below the expectations that were hyped up for ARM devices.

All of this is great news for FoxPro it extends the lifetime of FoxPro just a little longer yet... who would have thunk it?

this post created and published with the Markdown Monster Editor

Retrieving Images from the Clipboard Reliably in WPF Revisited

$
0
0

Banner Image

I've written previous about image formatting issues with the clipboard data in WPF and for the most part what I discussed in that post has been working just fine.

Recently however I started noticing some issues with some of my clipboard pasting code in Markdown Monster producing what appears to be corrupted images when displaying in an ImageSource control. The following is an image captured from Techsmith's SnagIt and then opened in Markdown Monster's Image dialog:

Bonked Image from ClipboardFigure 1 - Something's not quite right here ??

The image is sort of there, but it's obviously missing color, it's offset by a bunch with part of it missin, mis-sized and... well, it looks like an old, mis-tuned broadcast service TV image. It's an interesting effect, but not what that image should look like.

What's happening in this code is that the image is copied to the clipboard from SnagIt and then picked up by the form when it loads from the clipboard and loaded into an Image control via an ImageSource bitmap.

##AD##

That Nasty WPF ImageSource Control

It should be simple: Clipboard.GetImage() should just return you an ImageSource and that should be the end of it. Unfortunately, more times than not, that doesn't 'just' work. Anything a little off in the image and System.Windows.Clipboard.GetImage() fails. Either it outright crashes or worse - it returns a result, but the resulting ImageSourc doesn't actually render in the image control, which means your code has no way to really know that the image failed.

Turns out Windows.Forms.Clipboard.GetImage() works much more reliably, but it can't do transparency in .png images, which may be important.

So, in order to be able to handle all the use cases, I have been using a ClipboardHelper class with wrapper functions that look at the clipboard and explicitly extract the bitmap data first, and then create a new ImageSource from that bitmap data. That seems to be much more reliable. The first article goes into a lot of detail about how this works and also talks a bit about all the crazy clipboard formats you can run into and have to deal with if you're interested.

For this post, I'll show the complete, updated code for getting an ImageSource from the clipboard at the end of the post.

Image Formatting: Bitness

Long story short, the helper has been working for me for some time, except for the problem shown in Figure 1 that only occurs occasionally. After a bit of sleuthing in the code I was able to track this down to the conversion code that's converting the clipboard bitmap image data into an ImageSource to display in the WPF Image control. The bitmap itself appears to be fine as I checked the actual byte data and wrote it out to file. The file image looked perfectly fine.

So the issue is Bitmap to ImageSource conversion - which has always been the messy part of this entire process.

There are a number of ways to do this conversion:

  • Dumping the image into a byte stream and assigning loading the image source from that
  • Dumping to file and loading the image source from that
  • Using low-level locking of the actual image data and loading the image source from that

I'd been using the latter, because the in-place memory usage and unsafe code combination are wicked fast compared to any other approaches - like 3-4 faster, plus it doesn't require copying the bitmap buffer of a potentially large image into a second block of memory.

The original code was retrieved from Stack Overflow and I used it as is because to be honest I only had a vague idea what that code was actually doing. At the time I noticed some of the hardcoded values and thought about that being a problem but in a bit of testing I didn't see any issues with images from a variety of sources.

Here's the original SO code:

public static BitmapSource BitmapToBitmapSource(Bitmap bmp)
{
    var bitmapData = bmp.LockBits(
           new Rectangle(0, 0, bmp.Width, bmp.Height),
           ImageLockMode.ReadOnly, bmp.PixelFormat);

    var bitmapSource = BitmapSource.Create(
        bitmapData.Width, bitmapData.Height,
        bmp.HorizontalResolution, bmp.VerticalResolution,
        PixelFormats.Bgr24, null,
        bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);

    bmp.UnlockBits(bitmapData);

    return bitmapSource;
}

You'll notice in there that the PixelFormats.Bgr24 is hard coded. This will work most of the time but if the image is stored in 32 bit format you get - variable results. Oddly it works some of the time even with 32 bit images, but some images consistently failed with the image behavior shown in Figure 1.

To get around this hard coding, we can add a conversion routine that translates pixel formats between the System.Drawing and System.Windows values. The code shows both directions but only the first is from System.Drawing to System.Windows is actually used:

public static System.Windows.Media.PixelFormat ConvertPixelFormat(System.Drawing.Imaging.PixelFormat systemDrawingFormat)
{
    switch (systemDrawingFormat)
    {
        case PixelFormat.Format32bppArgb:
            return PixelFormats.Bgra32;
        case PixelFormat.Format32bppRgb:
            return PixelFormats.Bgr32;
        case PixelFormat.Format24bppRgb:
            return PixelFormats.Bgr24;
        case PixelFormat.Format16bppRgb565:
            return PixelFormats.Bgr565;
        case PixelFormat.Format16bppArgb1555:
            return PixelFormats.Bgr555;
        case PixelFormat.Format8bppIndexed:
            return PixelFormats.Gray8;
        case PixelFormat.Format1bppIndexed:
            return PixelFormats.BlackWhite;
        case PixelFormat.Format16bppGrayScale:
            return PixelFormats.Gray16;
        default:
            return PixelFormats.Bgr24;
    }
}

public static System.Drawing.Imaging.PixelFormat ConvertPixelFormat(System.Windows.Media.PixelFormat wpfFormat)
{
    if (wpfFormat == PixelFormats.Bgra32)
        return PixelFormat.Format32bppArgb;
    if (wpfFormat == PixelFormats.Bgr32)
        return PixelFormat.Format32bppRgb;
    if (wpfFormat == PixelFormats.Bgr24)
        return PixelFormat.Format24bppRgb;
    if (wpfFormat == PixelFormats.Bgr565)
        return PixelFormat.Format16bppRgb565;
    if (wpfFormat == PixelFormats.Bgr555)
        return PixelFormat.Format16bppArgb1555;
    if (wpfFormat == PixelFormats.Gray8)
        return PixelFormat.Format8bppIndexed;
    if (wpfFormat == PixelFormats.Gray16)
        return PixelFormat.Format16bppGrayScale;
    if (wpfFormat == PixelFormats.BlackWhite)
        return PixelFormat.Format1bppIndexed;

    return PixelFormat.Format24bppRgb;
}

And with that we can now fix the image Bitmap to ImageSource conversion:

public static BitmapSource BitmapToBitmapSource(Bitmap bmp)
{
    var bitmapData = bmp.LockBits(
           new Rectangle(0, 0, bmp.Width, bmp.Height),
           ImageLockMode.ReadOnly, bmp.PixelFormat);

    var pf = ConvertPixelFormat(bmp.PixelFormat);

    var bitmapSource = BitmapSource.Create(
        bitmapData.Width, bitmapData.Height,
        bmp.HorizontalResolution, bmp.VerticalResolution,
        pf, null,
        bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);

    bmp.UnlockBits(bitmapData);

    return bitmapSource;
}

Et voila: The image display now works correctly for anything I'm throwing at it. Here's the image rendering correctly in the Image dialog in Markdown Monster:

Un Bonked Image
Figure 2 - Properly captured image after fixing the Pixelmode conversion.

Note that this method requires unsafe code (for the LockBits call) so this may or may not be usable everywhere. If you need a version that works with safe code only you can use the following which uses an intermediary Memory stream:

public static BitmapSource BitmapToBitmapSourceSafe(Bitmap bmp)
{
    using var ms = new MemoryStream();
    
    bmp.Save(ms, ImageFormat.Png);
    ms.Position = 0;
    
    var bitmap = new BitmapImage();
    bitmap.BeginInit();
    bitmap.CacheOption = BitmapCacheOption.OnLoad; // Load image immediately
    bitmap.StreamSource = ms;
    bitmap.EndInit();
    bitmap.Freeze(); // Make the BitmapImage cross-thread accessible

    return bitmap;
}

Performance

This code is reliable but it's considerably slower (2-4 times slower depending on how many images you load) and also loads another copy of the image data for each image.

Note that there are a number of other ways to do this: Dumping to disk, traversing the Bitmap buffer with .NET safe code (which apparently is also fairly slow).

I'm using this method in Markdown Monster in a number of places and for single images displayed the slower processing and memory usage is not a problem. However, I also use if for an image list that displays a history of AI Image generated which contains potentially hundreds of images that are loaded asynchronously and the performance difference there is enormous:

Lots Of Images In An Image ListFigure 3 - In a large list of images load performance matters - so the faster algorithm using unsafe code ends up being 3-4 times faster.

The image slider on the bottom contains a large number of images that are loaded asynchronously in the background, but even though they are loading in the background the initial hit of the UI activating is as much as 4 times slower with the MemoryStream code than using the LockBits code.

Putting it all Together: Getting an Image or ImageSource off the Clipboard

I went into great detail about clipboard image retrieval in the previous post and some of the issues you need to deal with. I'm not going to rehash all of it here, but if you're interested there's tons of detail of why this can be such a pain in the ass.

The short version is: The Windows.Forms Clipboard works great, but it can't do transparency. The native WPF clipboard is super flakey with some image types.

The wrappers I show here make it very easy to retrieve Clipboard data safely and quickly and as reliably as possible. At this point I've thrown a huge number of different image types at the updated code and I've not had any failures other than a few out memory errors with very large images.

There are two functions:

  • GetImage()
  • GetImageSource()

as well as the previously shown methods:

  • BitmapToBitmapSource()
  • ConvertPixelFormat()

The whole thing to retrieve an ImageSource and a Bitmap Image first, looks like this:

// ClipboardHelper.GetImageSource()
public static ImageSource GetImageSource()
{
    if (!Clipboard.ContainsImage())
        return null;

    // no try to get a Bitmap and then convert to BitmapSource
    using (var bmp = GetImage())
    {
        if (bmp == null)
            return null;

        return WindowUtilities.BitmapToBitmapSource(bmp);
    }
}

// ClipboardHelper.GetImage()
public static Bitmap GetImage()
{
    try
    {
        var dataObject = Clipboard.GetDataObject();

        var formats = dataObject.GetFormats(true);
        if (formats == null || formats.Length == 0)
            return null;

        var first = formats[0];

        #if DEBUG   // show all formats of the image pasted
        foreach (var f in formats)
            Debug.WriteLine(" - " + f.ToString());
        #endif
        
        Bitmap bitmap = null;

        // Use this first as this gives you transparency!
        if (formats.Contains("PNG"))
        {
            using MemoryStream ms = (MemoryStream)dataObject.GetData("PNG");
            ms.Position = 0;
            return (Bitmap)new Bitmap(ms);
        }
        if (formats.Contains("System.Drawing.Bitmap"))
        {
            return (Bitmap)dataObject.GetData("System.Drawing.Bitmap");                    
        }
        if (formats.Contains(DataFormats.Bitmap))
        {
            return (Bitmap)dataObject.GetData(DataFormats.Bitmap);                    
        }

        // just use GetImage() - 
        // retry multiple times to work around Windows timing
        BitmapSource src = null;
        for (int i = 0; i < 5; i++)
        {
            try
            {
                // This is notoriously unreliable so retry multiple time if it fails
                src = Clipboard.GetImage();
                break;  // success
            }
            catch
            {
                Thread.Sleep(10);  // retry
            }
        }

        if (src == null)
        {
            try
            {
                Debug.WriteLine("Clipboard Fall through - use WinForms");
                return System.Windows.Forms.Clipboard.GetImage() as Bitmap;
            }
            catch
            {
                return null;
            }
        }
            
        return WindowUtilities.BitmapSourceToBitmap(src);
    }
    catch
    {
        return null;
    }
}

The code looks for a few known formats that can be directly converted from the raw clipboard data and those are immediately returned. Note that some formats are already in Bitmap format while others like PNG are a binary stream that has to be loaded and assigned to a Bitmap.

The first check is for PNG because we want to try and capture the transparency of the raw PNG image.

Next, both Bitmap or System.Drawing.Bitmap formats are actually stored in the raw .NET Bitmap format and directly cast to that type. Since this is the System.Drawing type transparency is not easily respected - that's why PNG is checked for first.

Otherwise the code tries to just read the WPF clipboard and see if that works. If it fails it will delay briefly and try again a few times. There's apparently a timing bug in the Windows Clipboard API that can cause the clipboard to not be ready to get the value out and retrying often can get the image. The native Windows.Forms.Clipboard functionality does that automatically and internally, but for WPF it needs to be explicitly coded.

If the WPF GetImage() fails, the code then falls back to Windows Forms GetImage() operation. Note that there might still be scenarios where the WPF GetImage() doesn't fail with an exception, but there's no image in the ImageSource to display.

I find that the vast majority of the cases are covered by either the PNG or one of the two Bitmap modes, so most code never goes to the WPF GetImage() call that is problematic.

##AD##

The other important part is that all the Clipboard operations (except the direct reads) are wrapped in exception handling, and if the WPF version fails, it falls back to the Windows Forms version. This works for any non-transparent image type, and for transparent ones - well it's better than no image at all I suppose ??

Prior to the Windows Forms fallback I was running into frequent clipboard pasting failures with images, and the fallback catches those cases that WPF can't handle or is too finicky about. While it may miss opacity, at least it will return something (and opacity is rarely an issue).

And with that in place, I now have proper working images from any source and including transparency:

Working Image From Clipboard
Figure 4 - Example of a captured 32bit image with transparency.

Yay!

Summary

WPF Image Clipboard operations have been notoriously difficult to work with, but with this circuitous workaround that first loads a bitmap and then an ImageSource from that, it's possible to load most images reliably. For those that still fail, the fallback to Windows.Forms.Clipboard usually captures the rest.

If you don't need to capture transparent images, you can bypass all this madness and simply use Windows.Forms.Clipboard.GetImage() with a try/catch block around it. This works 99.9% of the time and is easier.

Web Connection 7.32 released

$
0
0

Web Connection 7.32 is out and in this post I'll go over some of the new features. This release is a small update, mostly with bug fixes and a few small adjustments to existing functionality. Some of these are quite productive if you use them, but all are relatively minor enhancements. There are no breaking changes.

wwDotnetBridge Enhancements

.NET integration is becoming ever more important for integration with Windows and third party tools and libraries. Web Connection internally is starting to use more and more .NET related features and wwDotnetBridge over the years has become a key feature of the Web Connection library to support helper and support functionality from features like JSON Serialization, to encryption, to the email service functionality as well as small features like Unicode string handling support, UTC date conversions, advanced formatting for dates and number and much more.

So it's no surprise that there are many improvements that are thrown onto this library to make it easier to use and make integration with .NET code easier.

Improved Collection Support

.NET makes extensive use of various collection types and in this release it gets a bit easier to access collection members and set collection values using simple loList.Add(loItem) (or AddItem()) functionality. Likewise you can also add new Dictionary items - lists are indexed lists, while dictionaries are key/value collections - using the loList.AddDictionaryItem(lvKey, loItem). There's also a new RemoveItem() method that matches the native dictionary methods.

All of this makes use of collections more natural and like you would see in .NET code examples thereby reducing some of the impedence mismatch between FoxPro and .NET. That isn't to say, wwDotnetBridge code works just like .NET but it makes things a little more transparent.

Auto-Instance Parameter Fix Ups

As you probably know .NET features many types that FoxPro and COM can't directly pass to or receive from .NET, and wwDotnetBridge provides a ComValue wrapper object that can be used to 'wrap' a .NET type in such a way that you can receive it in FoxPro, update the value and pass back the wrapper in lieu of the actual .NET value. This allows the type to stay in .NET and therefore work within the confines of FoxPro code via indirect reference.

Some work has been done to make these wrapper ComValue objects more transparent when they are passed to .NET using the intrinsic Invoke() and SetProperty() methods. ComValue are now automatically unwrapped and can be treated like an actual .NET value passed again making it more natural to some of the wwDotnetBridge abstractions. Previously you had to manually unwrap the value and pass the value explicitly which in some cases also would not work. In most cases this should now work transparently.

JSON Serializer Improvements

Another hot feature of Web Connection are REST Services and by extension the JSON Serialization support in the framework. JSON Serialization is what makes it possible to turn FoxPro objects into JSON and pass it to a remote service and Deserialization allows receiving of JSON data and turning it back into FoxPro objects for use in REST Service methods or for manual deserialization.

This update includes some updates that remove some of the naming restrictions for JSON objects based on the EMPTY class. By default the JSON Serializer has to exclude some property names from Serialization, because some property names are FoxPro reserved names. For example, Name, Classname, Class, Id etc. Every base object has some of these properties and by default Web Connection filters out these known base properties.

Well, it turns out that if you create an EMPTY object, it has no base properties at all, so these filters are not really required. In this update the serializer checks for EMPTY objects and if it is renders the objects as is without any property name filtering resulting both in a clean JSON export as well as improved performance as the filtering operation doesn't have to be performed.

As a recommendation: When generating JSON output for serialization, it's highly recommended that you create objects based on EMPTY (or SCATTER NAME MEMO) for serialization to ensure that your property names are preserved.

loPerson = CREATEOBJECT("EMPTY")
ADDPROPERTY(loPerson, "firstname", "Rick")
ADDPROPERTY(loPerson, "lastname", "Strahl")
ADDPROPERTY(loPerson, "address", CREATEOBJECT("EMPTY"))
ADDPROPERTY(loPerson.Address, "street", "123 North End")
ADDPROPERTY(loPerson.Address, "city", "Nowhere")

loSer = CREATEOBJECT("wwJsonSerializer")
loSer.PropertyNameOverrides = "lastName,firstName"  && force case
lcJson = loSer.Serialize(loPerson)

As an aside, wwJsonSerializer internally uses EMPTY objects when creating cursor and collection items so the optimization is already prevalent. The recommendation is primarily for top level objects that you expose to the serializer.

JSON UTC Date Conversion Fix

The wwJsonSerializer::AssumeUtcDates flag can be used to specify that dates that you are passing as input are already UTC dates, and are not converted to UTC when serialized.

By default the serializer assumes that dates are local dates, and when serializing turns the JSON dates into UTC dates (using the generic Z postfix to denote UTC date. Then when the date is deserialized it's turned back into a local date.

Although this flag has been around for quite some time, it wasn't actually working and some people had been reporting problems dealing with dates that shouldn't be converted.

Web Connection Framework Features

GetUrlEncodedCollection() to parse URL Encoded Lists

Web Connection has always included support for parsing form variables into collections using Request.GetFormVarCollection() and - prior to that the now deprecated Request.aFormVars(). But if you also wanted to get a collection of all the QueryString or ServerVariables you were out of luck, having to manually parse the string values and decoding.

In this release the Request.GetUrlEncodedCollection() function is a generic method that can be used to take any URL encoded string of key value pairs and parse it into a decoded collection of key\values.

loQueryStrings = Request.GetUrlEncodedCollection(Request.cQueryString)
FOR EACH loQuery in loQueryStrings FOXOBJECT
    ? loQuery.Key
    ? loQuery.Value
ENDFOR

Security is important and standards for cookie security have been changing a lot in recent years, so the latest releases of Web Connection make the default cookie configuration setting a bit more strict to ensure your sites are not flagged as insecure by even basic security scanning tools.

A couple of changes have been made in default Cookie policy:

  • Default is cookie is set to HttpOnly
    This ensures that cookies cannot be read and modified on the client side. They are sent as part of the request and applied, but the cookie is not available for capture and reuse in client side code which avoid drive-by capture of cookies for replay attacks.

  • Default is set to samesite=strict;
    Likewise same site cookie policy is recommdended by default to avoid bleeding out cookies for capture outside of the currrent site. In most cases samesite=strict; should work fine, unless you building a federated login system where cookies are shared across sites. The new value is the sensible default to use for any cookies created.

These cookies values are applied when:

  • Creating a new Cookie with CreateObject("wwCookie")
  • Using Response.AddCookie()
  • Using Session Cookies in Process.InitializeSession()

Note that you can always override the cookie - AddCookie() returns the cookie instance and you can override any values as needed if you manually create it. Likewise you can override Process.InitializeSession() to explicitly specify your own cookie policy.

Summary

There you have it - Web Connection 7.32 changes (and a couple of 7.30 changes as well) are essentially maintenance update features. Some of these are highly useful if you are using these feature as they make life a lot easier. The Cookie settings are a necessary security update, which is one of the reasons of why you should try to keep up with updates of the framework to ensure you have the latest fixes and security updates.

Until next update...

this post created and published with the Markdown Monster Editor
Viewing all 133 articles
Browse latest View live