Root-Relative Paths (~/path) in Web Connection
December 10, 2009 •
Path management in Web Applications is critical and it’s especially tricky if you’re dealing with applications that display content out of many sub-directories. Most of my applicatinos have at least a root folder (duh!) and usually an admin folder that pages or scripts run out of.
It seems easy enough to reference images, CSS and other resources simply with page-relative links like:
<img src="images/help.gif" alt="Get Help" />
This works just fine. But in some situations and especially in situations when you build some sort of reusable components like a User Control or Server Control, a reused script or template it’s not quite so easy to determine a relative path.
Imagine for a second that you have a user control that has a reference to the same image above, but you now want to embed that user control into a page in the root folder, and into another page in a subdirectory. In the root folder the above Image ref works just fine, but in the subfolder – not so much. In the subfolder you’d need:
<img src="../images/help.gif" alt="Get Help" />
But how do you consolidate this?
Root-Relative Paths
Web Connection Web Controls – which is based on the ASP.NET concept – supports a workaround for this issue by allowing root-relative paths to be used for URLs that are specified on Web Controls.
Root relative paths allow you to create location independent urls that work regardless from where within the application they are called. This means you can use a url with root-relative path syntax and get consistent results in any directory of the application which makes the url portable – easy to copy and/or use in other pages without changes. Web Connection supports root-relative paths in various ways. Here’s how.
For example, you can use an image control instead of the raw HTML image tag to solve the problem above like this:
<ww:wwWebImage runat="server" ID="imgHelp" ImageUrl= "~/images/help.gif" />
Notice the ~/images/help.gif path which is effectively means:
Use the applications base Virtual Path (something like /wconnect) instead of the ~/ prefix and then append the rest of the URL to it.
The end result of this is that you can simply specify ~/images/help.gif from any directory of the application and it will always generate /wconnect/images/help.gif regardless of the folder you’re in.
Root-Relative Paths in Web Connection
Root relative paths can be used in several ways in Web Connection
- Native Web Control Properties
All native Web Connection Web Controls and custom URL properties on them support root-relative paths so ImageUrl on the image control, NavigateUrl on HyperLink and button controls etc. all support the ~/ url syntax automatically. All custom controls implemented by third parties should also support this functionality for all URL attributes for consistency. - Manually via Control.ResolveUrl() and Process.ResolveUrl()
You can manually fix up paths in code using either Control.ResolveUrl("~/images/help.gif") or Process.ResolveUrl("~/images/help.gif"). Internally controls call these APIs to fix up paths and you can also call them from within code any time to Resolve a url. - Automatic Fixup of generated HTML that contains ~/ attributes (New Feature in 5.51)
Web Connection 5.51 and later also automatically post processing HTML output and replaces any occurrence of ~/ paths at the beginning of an attribute string (ie. = "~/) and expands the full URL if the output is of content type text/html and a Process object is in scope. This is fully automatic now and works even on client controls: <script src= "~/scripts/jquery.js" type="text/javascript"></script> will now automatically transform into /wconnect/scripts/jquery.js.
I’m fairly excited about this latter feature because you can now apply root-relative paths everywhere. As a bonus Visual Studio understands root-relative paths even in plain URLs and so properly includes file references for rendering in the designer using this syntax for Intellisense support of CSS and script Intellisense for JavaScript.
How does Url Resolution Work in Web Connection?
As mentioned url resolution happens in wwProcess::ResolveUrl(). There’s also Control.ResolveUrl but it just defers to Process.ResolveUrl() internally. To understand how it works it’s probably easiest to look at the code which is actually super simple:
************************************************************************
* wwProcess :: ResolveUrl
****************************************
FUNCTION ResolveUrl(lcUrl)
IF lcUrl != "~"
RETURN lcURL
ENDIF
RETURN STRTRAN(Process.cUrlBasePath + SUBSTR(lcUrl,2),"//","/")
ENDFUNC
* wwProcess :: ResolveUrl
The method simply looks at the URL passed and checks for the leading tilde (~) and if it finds one replaces it with the Process.cUrlBasePath.
cUrlBasePath is the tricky issue in this functionality, because there’s nothing in the Web Server request that can easily translate the current request url to a URL base path automatically just based on the IIS request returned. Unlike ASP.NET which has a Request.ApplicationPath property which returns something like /wconnect, raw ISAPI requests do not receive this information.
This means that Web Connection requires an explicit approach to figure out the path and the way this is done by using a configuration setting that is defined on the Process class’s configuration object.
************************************************************************
* wwProcess :: GetUrlBasePath
****************************************
*** Function: Method responsible for establishing the base path
*** for this application.
************************************************************************
FUNCTION GetUrlBasePath()
IF !EMPTY(THIS.cUrlBasePath)
RETURN this.cUrlBasePath
ENDIF
TRY
THIS.oConfig = EVALUATE("THIS.oServer.oConfig.o" + this.Class)
THIS.cUrlBasePath = THIS.oConfig.cVirtualPath
CATCH
ENDTRY
RETURN THIS.cUrlbasePath
ENDFUNC
* wwProcess :: GetUrlBasePath
Assuming we want to retrieve the wwDemo process class configuration we’d access:
THIS.oServer.oConfig.owwDemo.cVirtualPath
Where does this value come from? If you recall Web Connection by default creates a configuration class for each Process class it creates with the same name as the process class prefixed by an o. The wwServer instance has a master configuration object (this.oServer.oConfig) and this config object in turn holds configuration settings for each process class – in this case owwDemo. Each of these classes has a default set of properties of which cVirtualPath is one of them.
To put this into perspective the value can be found in the configuration file (wcdemo.ini):
[Wwdemo]
Datapath=C:\WWAPPS\WC3\wwDemo\
Htmlpagepath=c:\westwind\wconnect\
Virtualpath=/wconnect/
Which maps to the class defined in wcdemomain.prg:
DEFINE CLASS wwDemoConfig as RELATION
cHTMLPagePath = "d:\westwind\wconnect\"
cDATAPath = ".\wwDemo\"
cVirtualPath = "/wconnect/"
ENDDEFINE
This is where the value comes from. The idea is that as long as the cVirtualPath is set properly in the config file the value will be picked up by GetUrlBasePath() and can then be used by ResolveUrl.
Yeah, this is a little on the complex side, but I haven’t been able to find a better way to resolve the base path than manually specifying it. FWIW, all of this should be automatic – the only thing you have to do as a developer is change the cVirtualPath if it changes. The New Project and Process Wizards hook all of this up for you automatically.
Automatic URL Resolution for raw HTML content – A new Feature in Web Connection 5.51
As mentioned earlier there’s a new feature in 5.51 that automatically looks at HTML output generated with the wwPageResponse class and automatically expands any URLs that contain ~/ at the beginning. This is fully automatic and doesn’t require any code changes on your part, you just need to make sure that your application is using the wwPageResponse class which is the default for Web Connection. If you’re using the Web Control framework you’re already using this class, but you can also ensure that this happens by using the wwPageResponse or wwPageResponse40.
This is the default for new projects so nothing needs to be done for new projects. Old projects that explicitly use one of the old Response classes (wwResponseFile, wwResponseString) do not get this functionality because they are not guaranteed to be cached in memory first.
Web Connection basically adds this simple block of code into the wwPageResponse::Render method:
*** Fix up ~/ paths with UrlBasePath
IF THIS.contentType = "text/html" AND VARTYPE(Process) = "O"
lcBasePath = Process.GetUrlBasePath()
IF !EMPTY(lcBasePath)
this.cOutput = STRTRAN(this.cOutput,[= "~/],[="] + Process.cUrlBasePath)
ENDIF
ENDIF
Since STRTRAN() in VFP is a blazing fast operation even on large strings this doesn’t add any significant overhead and so this process has been integrated without any noticable performance penalty.
If you haven’t looked at root-relative paths before be sure to check them out – they can make life a lot easier when building any links that need to live in multiple pages whether its code in user controls, custom server controls or just code that you like to cut and paste between different pages.
Mike McDonald
December 11, 2009