Backbase & ASP.Net

ASP.Net logo

This example will demonstrate how to combine the Microsoft ASP.Net Server-Side Framework with the Backbase Client Framework. It will focus on making the adjustments required for integrating ASP.Net WebForms and UserControls in a Backbase-enabled AJAX application. It does not demonstrate how to create an AJAX application. If you are interested in seeing this code in action, please read the Creating an SPI with Backbase and ASP.NET tutorial. For more information on Ajaxifying existing ASP.Net applications, please see the Progressive Enhancement of ASP.Net applications tutorial.

Prerequisites and Intended Audience

This example is intended for anyone interested in understanding how to integrate ASP.Net WebForms and UserControls in a Backbase-enabled application.

Backbase Prerequisites

  • Backbase Client Framework
  • Intermediate level experience with the Backbase Client Framework and the Single Page Interface (SPI) model

ASP.Net Prerequisites

  • Microsoft .Net Framework v2.0 or higher
  • Microsoft Visual Studio 2005 or similar
  • Intermediate level experience with ASP.Net

For more information on the SPI model, please read the Technical Overview.

Overview

When integrating ASP.Net WebForms in a Backbase-enabled AJAX application, it is imperative to understand the concept of a Single Page Interface (SPI). The SPI model can be compared to the concept of UserControls in an ASP.Net application. In ASP.Net, UserControls allow you to update parts of the WebForm dynamically, in order to separate your application design from your (modular) logic. With the Backbase SPI model, this concept is brought to the client.

In this example, we will explore how to make ASP.Net UserControls part of the Backbase SPI application.

Backbase & ASP.Net

In a Backbase SPI AJAX application, the partial page updates are performed asynchronously. Instead of reloading the entire page, the Backbase Client Framework will load the UserControl and place it in the application DOM tree.

To facilitate this behavior, we need to make some adjustments to the ASP.Net System.Web.UI.Page class. Since ASP.Net expects to be serving an entire WebForm with each postback, we need to simulate certain page-like behavior. Additionally, we need to rewrite all control ID attributes, because Backbase expects control identifiers to be unique on the page.

Working with Page Fragments

When working with a Backbase application, the ASP.Net WebForm will no longer be your primary page. Your primary page will look more like the application setup that is demonstrated in the Beginner Tutorial.

Instead, you will load your ASP.Net WebForms as fragments of the Backbase application. These fragments can be loaded into the application using either the xi:include tag or bb.command.load.

On the server side, you can choose to create fragments in two ways: multiple WebForms or a single WebForm that functions as the container for UserControls. Either way, you must create a WebForm that is suitable for inclusion in a Backbase application.

For your convenience, we have created all required templates and classes to facilate this:

  • The Backbase.Net.Page Class
  • The SPIPage.aspx WebForm template

The Backbase.Net.Page Class

The Backbase.Net.Page class is derived from the System.Web.UI.Page class and overrides the Render() method. This is required in order to prepare the page output for inclusion in the Backbase application.

Overridding the Page Render() method

The System.Web.UI.Page.Render() method is called upon rendering of the ASP.Net Page object. It will translate the managed code into an XHTML string that will be sent to the browser. During the Render phase, dynamic content (like ASP.Net client-side scripts and styling) is added to the page DOM Tree . Additionally, information regarding the application and session state are saved in hidden input fields that will be sent to the server in case of a postback operation.

During the render phase, ASP.Net translates the server-side tags like <asp:label Text="SomeText" /> to XHTML. It will add an ID attribute to the rendered tags with the same value as the System.Web.UI.Control.ClientID property.

To ensure that the ASP.Net fragment can work with the backbase application, we need to re-render the ASP.Net output. We do this by overriding the Render() method:

protected override void Render(HtmlTextWriter writer)
{
        StringBuilder stringBuilder = new StringBuilder();
        StringWriter stringWriter = new StringWriter(stringBuilder);
        HtmlTextWriter htmlWriter = new HtmlTextWriter(stringWriter);

        base.Render(htmlWriter);
        string XHTML = stringBuilder.ToString();
        writer.Write(Backbase.Net.Common.RenderHtml(XHTML));
}

As you can see in the code above, we first call the base.Render with a new HtmlTextWriter instance called htmlWriter. Since we override the System.Web.UI.Page class, the base.Render() method is actually the native render method for ASP.Net pages. It will return to us the XHTML as produced by ASP.Net, which we store in a variable called XHTML.

Next, we will call our custom Backbase.Net.Common.RenderHtml() method. This method will load the XHTML into an System.Xml.XmlDocument instance for smart editing. This also allows us to perform a server-side check if the output is actually valid XHTML, a requirement for Backbase-enabled applications.

The re-rendering of ASP.Net pages consists of the following steps:

  1. Replacing HTML entities
  2. The Page.Header workaround
  3. Including external (dynamic) resources
  4. Resolving the ID attributes
  5. Renaming .Net client functions
Replacing HTML Entities

Before creating the System.Xml.XmlDocument instance based on our XHTML output, we need to resolve the HTML entities. HTML entities are common used entities like &nbsp;, &copy;, &reg; and &euro;. The definition for these entities are built into the browser, so they do not need to be specified in your HTML page. However, these entities are not supported by the XML processor. Therefore, we need to translate the textual entity to the corresponding unicode number, which is understood by the XML parser:

string[] entities = "nbsp,iexcl,cent,pound,curren,yen,brvbar,sect,uml,copy,ordf,laquo,not,shy,reg,macr,deg,plusmn,sup2,sup3,acute,micro,para,middot,cedil,sup1,ordm,raquo,frac14,frac12,frac34,iquest,Agrave,Aacute,Acirc,Atilde,Auml,Aring,AElig,Ccedil,Egrave,Eacute,Ecirc,Euml,Igrave,Iacute,Icirc,Iuml,ETH,Ntilde,Ograve,Oacute,Ocirc,Otilde,Ouml,times,Oslash,Ugrave,Uacute,Ucirc,Uuml,Yacute,THORN,szlig,agrave,aacute,acirc,atilde,auml,aring,aelig,ccedil,egrave,eacute,ecirc,euml,igrave,iacute,icirc,iuml,eth,ntilde,ograve,oacute,ocirc,otilde,ouml,divide,oslash,ugrave,uacute,ucirc,uuml,yacute,thorn,yuml".Split(',');
for (int i = 0; i &lt; entities.Length; i++)
        XHTML = XHTML.Replace("&amp;" + entities[i] + ";","&amp;#" + (160 + i) + ";");
XHTML = XHTML.Replace("&amp;euro;", "&#8364;");

Now we are ready to load the XHTML source into our XmlDocument instance:

// Try to load the XHTML source as XML document
XmlDocument Source = new XmlDocument();
try
{
        System.IO.StringReader SR = new System.IO.StringReader(XHTML);
        System.Xml.XmlReader XmlTextReader = new System.Xml.XmlTextReader(SR);
        Source.Load(XmlTextReader);
        XmlTextReader.Close(); SR.Close();
}
catch(System.Exception e)
{
        // Load Failed, throw error
        throw new System.Exception("The Page Source is not valid XHTML\r\n" + e.ToString() + "\r\n\r\nSource:\r\n" + XHTML);
}

For more information on HTML entities, please refer to Wikipedia.

The Page.Header Workaround

The first replacement we will be doing is fixing the Page.Header workaround. Some of the ASP.Net controls require the Page.Header property to access the <head /> tag to dynamically attach styling information and client-side code. However, because we are serving page fragments, and not the entire HTML page, the page <head /> tag is unavailable.

To solve this issue, we have added the <head runat="server" /> to our <form /> tag. Because this is not valid when following the XHTML schema, we need to remove this tag from the actual output sent to the browser:

XmlNode HeadNode = Source.SelectSingleNode("//*[local-name()='head']");
if (HeadNode != null)
{
        XmlNode DivNode = Source.CreateElement("div",HeadNode.NamespaceURI);
        foreach (XmlNode Node in HeadNode.ChildNodes)
                if (Node.Name.ToLower() != "title" &amp;&amp; Node.Name.ToLower() != "meta")
                        DivNode.AppendChild(Node.Clone());
        HeadNode.ParentNode.ReplaceChild(DivNode, HeadNode);
        DivNode = null; HeadNode = null;
}

To preserve the generated code inside the <head /> tag, we attach its childnode to a newly created <div />;. Finally, we replace the <head /> tag in the XML DOM tree with this <div />.

Including External (Dynamic) Resources

Since our main concern is replacing the ID attributes generated by ASP.Net, we need to make sure that these identifiers are not also used in external resources like <script /> tags or CSS styling (<style />). To allow us to re-render these external resources, we include them inline using the System.Net.WebRequest class.

// Gather online resources and place them inline for ID replacement
foreach (XmlNode IncludeNode in Source.SelectNodes("//*[local-name()='script' and @src]|//*[local-name()='link' and @href]"))
{
        string Content = null;
        if (IncludeNode.Attributes["src"] != null)
                Content = GetExternalResource(GetValidURI(IncludeNode.Attributes["src"].Value));
        if (IncludeNode.Attributes["href"] != null)
                Content = GetExternalResource(GetValidURI(IncludeNode.Attributes["href"].Value));

        if (Content != null)
        {
                IncludeNode.InnerText = Content;
                if (IncludeNode.Attributes["src"] != null)
                        IncludeNode.Attributes.Remove(IncludeNode.Attributes["src"]);
                if (IncludeNode.Attributes["href"] != null)
                        IncludeNode.Attributes.Remove(IncludeNode.Attributes["href"]);
        }
}

Here is the first supporting function:

private static string GetValidURI(string URI)
{
        string sResult = null;
        System.Web.HttpContext Current = System.Web.HttpContext.Current;
        if (!Uri.IsWellFormedUriString(URI, UriKind.Absolute))
        {
                string Root = Current.Request.Url.Scheme + "://" + Current.Request.Url.Host + ":" + Current.Request.Url.Port;
                if (URI.StartsWith("/"))
                        sResult = Root + URI;
                else
                {
                        string FullRelativePath = Current.Request.Url.AbsolutePath;
                        FullRelativePath = FullRelativePath.Substring(0, FullRelativePath.LastIndexOf("/") + 1);
                        sResult = Root + FullRelativePath + URI;
                }
        }

        //omit Backbase-related JavaScript files
        if (sResult.ToLower().Contains("/engine/") || sResult.ToLower().Contains("boot.js") || sResult.ToLower().Contains("backbase.js"))
                sResult = null;

        return sResult;
}

Here is the second supporting function:

private static string GetExternalResource(string URI)
{
        System.Net.WebRequest WReq = null;
        System.Net.WebResponse WResp = null;
        System.IO.StreamReader SR = null;
        string sResult = null;

        try
        {
                WReq = System.Net.WebRequest.Create(URI);
                WResp = WReq.GetResponse();
                SR = new System.IO.StreamReader(WResp.GetResponseStream());
        }
        catch { }

        if (SR != null)
        {
                sResult = SR.ReadToEnd();
                SR.Close(); WResp.Close();
        }
        SR = null; WResp = null; WReq = null;

        return sResult;
}

However, we are not finished yet. We have now included external resources as declared by the <script> and <link> tags. Unfortunalty, CSS allows for an additional method of external inclusion: @import. We need to search for this directive and include the external resources before we can resolve the ID attributes.

string output = Source.OuterXml;
while (output.ToLower().Contains("@import"))
{
        string Import = output.Substring(output.ToLower().IndexOf("@import")).Trim();
        Import = Import.Substring(0,Import.ToLower().IndexOf("\r\n")).Trim();
        if(Import.IndexOf(";") >= 0)
                Import = Import.Substring(0,Import.IndexOf(";")).Trim();
        Import = Import.Substring(8);

        string url = null;
        if (Import.StartsWith("\""))
                url = Import.Substring(1, Import.LastIndexOf("\"") - 1).Trim();
        else if (Import.ToLower().StartsWith("url('"))
                url = Import.Substring(5, Import.LastIndexOf("'") - 5).Trim();
        else if (Import.ToLower().StartsWith("url("))
                url = Import.Substring(4, Import.LastIndexOf(")") - 4).Trim();
        else
                url = Import.Trim();

        string Content = GetExternalResource(GetValidURI(url));
        if (Content != null)
        {
                string before = output.Substring(0, output.ToLower().IndexOf("@import"));
                string after = output.Substring(output.ToLower().IndexOf("@import"));
                if (after.Substring(0, after.IndexOf("\r\n")).Contains(";"))
                        output = before + Content + after.Substring(after.IndexOf(";"));
                else
                        output = before + Content + after.Substring(after.IndexOf("\r\n"));
        }
        else
        {
                //Something went wrong, resolve failed. Continue with further processing.
                break;
        }
}

Since CSS styling can be placed anywhere in the DOM tree, it would cost us too much time to use the XML parser for finding the @import directive. Therefore, we take the string from the XmlDocument using the OuterXml property. Next, we iterate throught the string, looking for each @import instance and replacing this with the content resolved using the GetValidURI() and GetExternalResource() functions.

Resolving the ID Attributes

With all the required content in one string variable, we can begin resolving the ID attributes. Our primary concern is that we gather all ID attributes first and create new random identifiers to replace them. For this we create an instance of a System.Collections.Specialized.NameValueCollection. This will hold both the old and the new identifiers, so that we can iterate through the collection at a later stage:

// Gather ID attributes and replace them with new unique identifiers
System.Collections.Specialized.NameValueCollection ControlIdentifiers = new System.Collections.Specialized.NameValueCollection();
foreach (XmlNode IDNode in Source.SelectNodes("//*[@id]"))
{
        string CurrentID = IDNode.Attributes["id"].Value;
        string NewBackbaseID = GetRandomID(CurrentID);
        ControlIdentifiers.Add(CurrentID, NewBackbaseID);
}

We use XPath with our XmlDocument to locate all instances of the ID attribute. Next, we use the support function GetRandomID() to generate a unique identifier:

private static string GetRandomID(string ID)
{
        Random randomize = new Random();
        string RandomID = randomize.Next(9999).ToString();
        while (RandomID.Length &lt; 4)
                RandomID = "0" + RandomID;
        RandomID = "BB_" + RandomID + "_" + DateTime.Now.Ticks.ToString() + "_" + ID;
        return RandomID;
}

Now that we have listed all ID attributes and their replacements, we will begin iterating through the list in order to replace them. Just like the CSS @import directive, we need to use the XML string retrieved from the OuterXml property to ensure we will replace all occurrences of the ID:

// Smart replace ID occurrences with new unique identifier
foreach (string ID in ControlIdentifiers.Keys)
{
        string tmp = output;
        string ReplacedOutput = "";
        while (tmp.Length > 0)
                if (tmp.IndexOf(ID) >= 0)
                {
                        string toId = tmp.Substring(0, tmp.IndexOf(ID));
                        string fromId = tmp.Substring(tmp.IndexOf(ID) + ID.Length);

                        if ((fromId.StartsWith(".") || fromId.StartsWith("\"") || fromId.StartsWith("'")) &amp;&amp; !toId.EndsWith("name=\""))
                                ReplacedOutput += toId + ControlIdentifiers[ID];
                        else
                                ReplacedOutput += toId + ID;
                        tmp = fromId;
                }
                else
                {
                        ReplacedOutput += tmp;
                        tmp = "";
                }
        if(ReplacedOutput != "")
                output = ReplacedOutput;
}

Replacing the identifiers can be a bit tricky. First of all, we cannot alter the name attribute. The value of the name attribute is used when submitting the form and is linked with the managed control on the server. If you change the name attribute, ASP.Net will no longer respond to event changes, because it cannot match the submitted elements with the managed controls.

Additionally, some ASP.Net controls create child elements that have the ID of their parent inside their own name. For instance, a System.Web.UI.TreeView instance called TreeView1 will attach dynamic JavaScript code that refers to a childnode as TreeView1n1. In this case, the ID TreeView1 is then replaced, not reflecting the correct ID of the childnode. Therefore, we added a check to ensure that we are dealing with a standalone ID.

Renaming .Net Client Functions

Now that the hard part is over, we only need to be concerned with the default ASP.Net client-side functions like __doPostBack. When we have multiple ASP.Net fragments in our application, these functions will overwrite each other in the client browser. We must replace each of their names with a unique identifier for our current page:

if (output.IndexOf("__doPostBack") > -1)
        output = output.Replace("__doPostBack", GetRandomID("__doPostBack"));

Finally, we need to change the form submission behavior. By default, the __doPostBack() function will call the form submit method. This will result in a normal submit and will reload the entire page.

Since we are now dealing with a form inside the Backbase application, we need it to implement our SPI submit() method:

if (output.LastIndexOf("theForm.submit();") != -1)
        output = output.Replace("theForm.submit();", "bb.getControllerFromView(theForm).submit();");
output = output.Replace("theForm", GetRandomID("theForm"));

While we are at it, we also rename the form variable to be unique for the current page, in order to ensure that not all ASP.Net forms are submitted once the __doPostBack() function is called.

We conclude the Backbase.Net.Common.RenderHtml() function by branding the output string and returning it to the Backbase.Net.Page, where it will be sent to the browser:

output = "<!-- Rendered By Backbase -->\r\n" + output;

// Return the rewritten source
return output;

This concludes the description of the Backbase.Net.Page class and Backbase.Net.Common.RenderHtml() function.

The SPIPage.aspx Template

For your convenience, we have created a template WebForm to use in case of integrating ASP.Net fragments in a Backbase application.

When working with fragments, you can choose the following scenarios:

  • Working with multiple WebForms
  • Working with a WebForm container for UserControls
The Template Explained

This section explains the inner workings of the template and what you should consider when adjusting it. The following features are part of the SPIPage template:

  1. First, we must stress that it is imperative that you add the <form /> tag somewhere in the template. We set it as the root tag, since we will be using it anyway.
  2. Make sure that your page is valid XHTML! This means that you need to declare all XML namespaces that you use.
  3. We have added an event handler for the form construct event. This is fired when the form is processed by the Backbase Client Framework:

    <e:handler event="construct" type="text/javascript">
            this.setAttributeNS("http://www.backbase.com/2007/forms","destination",".");
            this.setAttributeNS("http://www.backbase.com/2007/forms","mode","replace");
    </e:handler>

    Make sure that you keep this event handler. It transforms the standard XHTML form into a Backbase SPI form. When submitted, the form will be replaced by the fragment returned by the server.

  4. We added two convenience handlers that will help you create your application:

    <e:handler event="submitForm" type="text/javascript">
            this.submit();
    </e:handler>

    If you need the form to be submitted in your application, you can issue the "submitForm" event using the Backbase.Net.Page.DispatchClientEvent() method.

    <e:handler event="DOMNodeInsertedIntoDocument" type="text/javascript"><![CDATA[
            if(!this.viewNode.style.height) {
                    var sHeight;
                    var oNodes = this.getProperty("childNodes");
                    for(i=0;i < oNodes.length; i++)
                    {
                            var oElm = oNodes[i];
                            if(oElm.hasAttribute("height"))
                                    sHeight = oElm.getAttribute("height");
                            else if (oElm.viewNode)
                                    sHeight = oElm.viewNode.style.height;

                            if (sHeight == "100%") {
                                    this.viewNode.style.height = "100%";
                                    break;
                            } else if (sHeight.indexOf('%') > -1) {
                                    bb.command.trace(this,'Backbase .Net Warning: Style property height not set while childnodes have relative height. This might result in unexpected visualization of childNodes.');
                                    break;
                            }
                    }
                    if(oNodes.length > 0 && sHeight == '')
                            bb.command.trace(this,'Backbase .Net Warning: Could not determine the height of listed childnodes. This might result in unexpected visualization of childNodes.');
            }
    ]]></e:handler>

    This event handler will be executed when the form element is placed in the DOM tree. It will look for the height attribute on its child nodes and ensure that the form takes the same value. This is to avoid visualization issues, which are discussed in the Visualization Concerns section below.

  5. Do not remove the <head runat="server" /> tag. This is required for some ASP.Net controls to function correctly (as discussed earlier, this is part of the Page.Header workaround).
Working with Multiple WebForms

If you feel more comfortable working with WebForms instead of UserControls, you can choose to create a WebForm for each ASP.Net fragment included in your Backbase application. Simply copy the SPIPage template, rename it, and start coding.

However, keep in mind that there are some disadvantages to using multiple WebForms. For instance, if the SPIPage template changes, you will need to apply these changes to all your WebForm instances.

You can include your WebForms as fragments in your Backbase application using the following line:

<xi:include href="/WebForms/MyWebForm.aspx" />
Working with a WebForm Container for UserControls

The UserControl approach means that you only copy the SPIPage.aspx template once. Next, in the Page_Load() event handler, add the following code:

string path = Request.QueryString["ascx"];
if (path != "" &amp;&amp; System.IO.File.Exists(Server.Mappath(path)))
        spiform.Controls.Add(LoadControl(path));

You can include your UserControls as fragments in your Backbase application using the following line:

<xi:include href="/Content.aspx?ascx=UserControls/MyUserControl.ascx" />
Visualization Concerns

We have added an event handler to deal with possible visualization issues. Unfortunately, this will not solve everything and will only function as a first indicator that something might not be functioning as desired.

When creating a full screen application (height: 100%; width:100%) some controls might not show up on your page. If this occurs, it might be because one of your elements does not have its height specified.

The height of relative-positioned elements is based on the height of their parent element. If the element has a height of 100%, but the parent height is not specified, the control will not be visible, because 100% of null is null.

If you choose the SPI application model with ASP.Net fragments constructed using a single WebForm container and multiple UserControls, there is a possibility that many container elements (like <div />) will be nested inside each other. Make sure that their height and width styling properties are set to ensure the desired visualization of your pages.

Cache Control for Dynamic Pages

Another issue when dealing with ASP.Net fragments is the possibility of the caching of dynamic content, both on the client and on the server.

To avoid this, we have added the following code to the Backbase.Net.Page Page_Load event handler:

System.Web.HttpContext.Current.Response.AddHeader( "Cache-Control","no-cache");
System.Web.HttpContext.Current.Response.Expires = 0;
System.Web.HttpContext.Current.Response.Cache.SetNoStore();
System.Web.HttpContext.Current.Response.AddHeader( "Pragma", "no-cache");

This will tell the client browser not to cache the result of the request, thus ensuring that the page content is renewed each time.

In our templates, we do not set the server-side cache behavior. However, you can choose to control this using the OutputCache directive. The following code snippet will disable server-side cache:

<%@ OutputCache Duration="1" VaryByParam="*" NoStore="true" %>
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SPIPage.aspx.cs" Inherits="Backbase.SPIPage" %>      

Summary

In this example page, we have demonstrated how you can adjust the ASP.Net rendering method to ensure that your ASP.Net WebForms and UserControls can be used as page fragments inside a Backbase-enabled application.

Additional Resources

Disclaimer

The source code attached to this tutorial is published under the GNU Lesser General Public License v3 and is provided without support or any warranty. The LGPL conditions allow you to use the source code in your enterprise application as well as redistribute and modify the source code to your discretion.

You will find the following copyright text in every file in the attachment, as well as a copy of the GNU GPL and LGPL licenses.

//  Copyright 2008 Backbase B.V.
//  This document is part of the Backbase ASP.Net Connector
//  and is distributed under the terms of the GNU Lesser General Public License.
//  Backbase does not provide support for the ASP.Net Connector.
//
//  The Backbase ASP.Net Connector is free software: you can redistribute it
//  and/or modify it under the terms of the GNU Lesser General Public License
//  as published by the Free Software Foundation, either version 3 of the License,
//  or (at your option) any later version.
//
//  The Backbase ASP.Net Connector is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
//  or FITNESS FOR A PARTICULAR PURPOSE.  
//  See the GNU Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public License
//  along with the Backbase ASP.Net Connector. If not, see <http://www.gnu.org/licenses/>.
//
//  For code examples, see <http://bdn.backbase.com>
//  You can contact us at <http://www.backbase.com>

Download

Download the ASP.Net Connector (contains Backbase.Net.Page Class and SPIPage.aspx WebForm template)

AttachmentSize
Backbase_ASPNet_Connector.zip261.59 KB