Meaningful URLs and History Management

HTTP Request Handler

Studio Support for Web Application Development

Introduction

ASP-based web applications functioned using full page reloads. Server-side scripts steered by GET parameters in the URL would generate a page showing the proper data. For example, the URL for showing customer 12 might look like this: ‘customer.asp?number=12’. Going from a list of customers to a specific customer would mean navigating from the page ‘customeroverview.asp’ to ‘customer.asp?number=13’. The browser relies on this page reload to record history. Clicking on the back button would simply load the previous URL in the browser history.

Modern web applications do not rely on page reloads; instead they function using JavaScript and only refresh parts of the page. This is referred to as ‘single page applications’ and the DataFlex WebApp Framework uses this single page technology.

While this provides an improved user interface and allows nicer development styles, it has two major side effects.

  1. The browser no longer automatically records history.
    This causes the back button to behave different than the user expects; instead of going a step back in the application, it tends to leave the application completely.
  1. These applications by default don’t support deep links into the application.
    You cannot copy and paste URLs to a new browser window and get the same view with the same record.

To address this, browser vendors implemented APIs that allow custom manipulation of the browser history from JavaScript. This allows single page applications to add history items, and react to the back button appropriately. It also allows applications to change the URL in the address bar without actually reloading the page.

DataFlex Web Applications

The DataFlex WebApp Framework is capable of generating deep links and can use the browser its history management API’s to generate history items. These URLs point to a specific view in the application and in case of the drill-down model they are based on the view stack. The string that extends the URL with this information is called the state hash.

The state hash represents the current state of the application. For desktop style this is usually the name of the view and a record id ‘Customer-212’ or ‘Inventory-CUA’. For drill-down style applications this can be a longer path like ‘Inventory/Product-CUA’ or ‘OrdersByCustomer/Orders-99/Order-2152’. The full state hash for a drill down application is a combination of multiple view hashes.

Each operation in the application (navigating a view or finding a record) can push new history items to the stack or replace the top-level item. If a deep link is opened then the framework reproduces the state by performing the navigation operations in the background based on the state hash. The navigational system of the drill-down model handles this reliably and securely by registering all possible navigation paths (using WebRegisterPath).

The history management logic can be turned on or off using the peApplicationStateMode property on the cWebApp object. By default, history management is turned off so that migrated projects will behave as they always did (and no changes are required). For new projects the history management will be turned on by default and the Studio and wizards will generate the required code.

State Hashes

The part of the URL that is generated by the framework to represent the current state of the application is called the state hash. This hash is generated when certain operations (like changing a view) are performed in the application. The URL with the updated state hash is pushed onto the history stack or replaces the top-level item depending on the operation.

Desktop Style

For desktop-style applications there is always only a single view in the state hash (the currently displayed view). The hash for this view consists of the following parts:

{viewname}-{recordid}-{customdata}

When switching between views a new history item is pushed onto the stack. If the main record of the view changes then the top item is replaced with the new hash. So, the back button always goes to the previous view, and not to the previous record.

In the example URL http://localhost/WebOrder_19_1/Index.html#Order-107 the state hash is ‘Order-107’. See the Custom URL Format section for information on how to customize how the state hash is embedded into the URL.

The viewname uniquely identifies the view in the application and is determined by the psStateViewName property. It defaults to the object name.

The recordid is the record id of the main data dictionary for the view. Which fields are included into this id is determined by the piPrimaryIndex property of the DataDictionary. The GenerateStateHashRecordId and HashRecordIdToRowId messages of the view can be augmented to customize this behavior.

The customdata can be provided by the developer by implementing OnDefineCustomStateHash and can be queried using CustomStateHash.

When a desktop-style deep link is opened the framework will load the view of the URL (instead of the default view) and finds the main record. OnShow is sent as usual (which is were you could use CustomStateHash). If the user is not logged in yet the login view will be shown before the user is sent to the view in the link.

Drill-Down Style

In drill-down applications the user navigates through multiple views building up the view stack (or bread-crumb trail) as they go. When opening a deep link, the framework has to go through all the navigation steps of the view stack to recreate the proper state. To be able to do this, the state hash for drill-down applications consists of multiple state hashes, together forming the view stack. The format of the state hash is

{navigationpathid}-{recordid}-{customdata}/{navigationpathid}-{recordid}-{customdata}

An example hash is

OrdersByCustomer/Order-2175/OrderLine-2175_1

The navigationpathid identifies the navigation operation being performed and it allows the system to identify which view it needs to navigate into, the invoking object and the navigation type. This information is provided by registering the navigation path using WebRegisterPath. Because all paths are registered the system can also verify that the path in the URL is a valid path. The navigationpathid defaults to the psStateViewName, but if multiple paths between the invoking view and the navigate forward view exists a number is added to make the path name unique. A custom path name can be provided with the WebRegistPath command as well.

The {recordid} is added for zoom views and for some select views. For select views the record id is only added if the view is navigated into using nfFromChild, nfFromParent or nfFromMain. In that case the view state hash actually contains the recordid of its invoking view. The recordid is the record id of the main data dictionary for the view. Which fields are included into this id is determined by the piPrimaryIndex property of the DataDictionary. The GenerateStateHashRecordId and HashRecordIdToRowId messages of the view can be augmented to customize this behavior.

The customdata can be provided by the developer by implementing OnDefineCustomStateHash and can be queried using CustomStateHash.

When a desktop-style deep link is opened the framework performs the navigation options and actually sends the OnGetNavigateForwardData event to the invoking object. It also sends the OnNavigateForward event to the view it is navigating into (which is where you could use CustomStateHash). If the user is not logged in yet the login view will be shown before the user is sent to the view in the deep link.

Adding Additional Data to the State Hash

Additional information can be added to the URL (or state hash) using the OnDefineCustomStateHash API at view level. The information can be queried when opening a URL using the CustomStateHash function. This is expected to be done in OnShow or OnNavigateForward to reinitialize the state.

The following example shows how to add and restore information in a custom web property named psFilterValue.

Procedure OnDefineCustomStateHash String ByRef sStateHash

    WebGet psFilterValue to sStateHash

End_Procedure

Then inside OnNavigateForward we would restore the filter value by reading the custom state hash.

Procedure OnNavigateForward tWebNavigateData NavigateData Handle hoInvokingView Handle hoInvokingObject

    String sStateHash     

    Get CustomStateHash to sStateHash

       

    If (Trim(sStateHash) <> "") Begin

        WebSet psFilterValue to sStateHash

    End

End_Procedure

Sometimes you also want to put values from the NamedValues array of the navigation data into the custom state hash. That is only necessary if the OnGetNavigateForwardData message is not able to supply that information. That might be because it reads it from a non-data aware field or so. In a lot of cases (static data, or record buffers) it will provide the right data and adding it to the custom state hash is not necessary.

Testing

The deep links are a new way of accessing parts of your application. It is good practice to see if this behaves as you would expect. The simplest way to test a deep link is by using the refresh button. When doing that you’ll see the page being reloaded and the application state being restored. It is important to realize that not all information has to be restored. The user will expect unsaved changes to be lost, and in a header-detail view it isn’t always necessary to reselect the same detail record.

Building History

History is built up automatically on specific operations. The framework automatically triggers the creation of a new history item when switching views or when a new main record is found. This update can be triggered manually by calling the UpdateStateHash procedure on the current view. The bReplace parameter indicates if a new history item should be added (False) or if the current item needs to be replaced. The UpdateStateHash triggers the generation of a state hash by calling GenerateStateHash that will eventually trigger OnDefineCustomStateHash.

The system can be customized in several ways. At the highest level, it can be turned off using peApplicationStateMode. An alternative supported mode is available by setting the mode to asmHistoryOnly, which tells the framework to not update the URL, but still add history items. In that case, the back button will work, but deep links are not supported, thus refreshing the page will still make the application jump back to the dashboard.

Page Titles

The page title is a title provided by a web page that is shown by the browser. Modern desktop browsers usually show it on the tab button and on the start bar. Browsers can sometimes also show a list of history items. With history management, it becomes more important to update the page title with sensible information about the application state. DataFlex WebApps update the page title automatically when switching views. Usually this happens after a history item is added. For drill-down applications, the title is also updated when SetHeaderCaption is called. This update can be triggered manually by calling UpdatePageTitle in the current view.

By default, the page title format is { Application Title } - { View Caption }. This format can be customized by implementing OnGeneratePageTitle in the oWebApp. The application title can be configured by setting the psApplicationTitle property in the WebApp object.

Custom URL Format

By default, the state hash is inserted into the URL as the URL hash. The hash part of the URL is the last part of the URL starting with the # symbol. Historically this is used to indicate a location within a web page and modern JavaScript WebApps (Applications in a Page) started to use this to indicate a place in their application. By default, DataFlex also uses the hash part of the URL for the state hash. For example:

http://localhost/WebOrderMobile_19_1/#Orders/Order-2220

One of the big advantages of doing this is that it does not require additional configuration of the web server (Internet Information Server).

To change the URL format, the developer will have to implement two methods in DataFlex: StateHashToUrl and StateHash. The StateHashToUrl procedure is called each time a history item is added or updated. It gets the hash as a ByRef parameter and alters this into the URL. Default implementation of this procedure adds the # upfront, so that “Orders/Order-2220” is changed into “#Orders/Order-2220”, which the browser will interpret relatively to the existing URL (so we don’t need to provide the entire URL).

Friendly URLs without #

If we wanted the URL to be “http://myordersys.mydomain.com/Orders/Order-2220“, then we could override StateHashToUrl with the following implementation:

Procedure StateHashToUrl String ByRef sHashUrl

    If (Left(sHashUrl, 1) <> “/”) Begin

        Move (“/” + sHashUrl) to sHashUrl

    End

End_Procedure

The second function we need to override is StateHash, which is used by the framework get. By default, this function returns the hash which is sent to the server as a separate web property, but now we need to get it from the URL. The following example shows how this could be done:

Function StateHash Returns String

    String sHash sURL

   

    //  Get the URL of the page that sent the AJAX request

    Get ServerVariable of ghoWebServiceDispatcher “HTTP_REFERER” to sURL

   

    //  We assume everything after the first slash (not counting the http://)  is the state hash

    Move (Right(sURL, Length(sURL) - Pos(“/”, sURL, 8))) to sHash

   

    Function_Return sHash

End_Function

When doing this, you probably also want to set the pbStateAsUrlHash property to False, because when working with the URL hash, the framework also responds to the OnLocationHashChange event for updating the current location (when changing the location hash in a browser, its address bar it usually does not reload the page but triggers this event).

To make this work, we’d also have to configure IIS (for example using the URL rewrite module) to make this URL still end up at our index.html. You could do this by creating a redirect URL with the regular expression ^([^/]*/?)+ forwarding to Index.html.

Also make sure that the ‘Is Not a File’ and ‘Is Not a Directory’ are conditions are active.