Request a topic or
contact an Arke consultant
404-812-3123
ASP.NET click tracking

Arke Systems Blog

Useful technical and business information straight from Arke.

About the author

Author Name is someone.
E-mail me Send mail

Recent comments

Archive

Authors

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2022

ASP.NET click tracking

In order to integrate some classic ASP pages with an ASP.NET site, I need the user to go through an intermediate session copy page every time they click on a link to an asp page. 

This requirement is also very common in ‘click tracking’, where you want to keep track of what outgoing links users have clicked on from your site.  You do this by sending them through an intermediate page before redirecting them out of the site.

This leads to a lot of ugly links like:

http://demo/util/sessionupdate.aspx?url=%2targetdir%2findex.asp

When previously they were clear links like:

http://demo/targetdir/index.asp

How can we get back our clear links while still going through the intermediate page every time?

Before clicking:

image

After clicking:

image

One of the earliest approaches was to use javascript to change the status bar.  This led to a lot of fake urls so it was widely disabled by the browser makers – IE6 is the last major browser where this approach still works. 

Another approach is to use javascript ‘onclick’ to redirect the user; onclick runs before the href is followed, and if you return false from it the href won’t be followed, and you can use a javascript redirect like window.location.  This tends to work, but only for users with javascript enabled, and I believe there are some instances where it doesn’t really work.  In my specific case, I need to always send the user through the intermediate page.  In general, it’s cleaner to send the user only where the href actually links them.

Search engines like Google, Yahoo and Bing are all currently using a javascript ‘onmousedown’ event to respond to the users clicks.  Yahoo and Google both serve up the regular href, so that your status bar has the nice clear link, but when you mouse down they swap it out with their click tracking page.  (Bing instead kicks off an AJAX xmlhttprequest as soon as you mousedown.) 

Before clicking:

image

After mouse down:

image

These approaches have the advantage of serving links up in a fail-safe works-fine-with-javascript-off manner, and also only tracking real clicks.  However, in my specific case, my fail-safe is the opposite: I need to go through the intermediate page if javascript is off.

So, my solution is to serve up the links with the intermediate page.  Then if the user has javascript enabled I use javascript to replace the intermediate link with the clear direct link, and in onmousedown I swap it back again replacing the clear direct link again with the intermediate link.  End result is everybody should get gets session links, and users with javascript see nice clear links in their status bars.

Below is an asp.net user control that makes these click tracking links.

Register controls in web.config:

    <pages buffer="true" validateRequest="true">
      <controls>
        <add tagPrefix="uc" namespace="arkelibrary.controls" assembly="arkelibrary"/>
      </controls>
    </pages>

Using it on a page:

    <!-- specialchars are: ?&<z>![test]+ -->
    <fieldset>
    <legend>Click Tracking with JavaScript</legend>
    <uc:asplink runat="server" href="/webtest/asppage.asp?param1=foo&param2=specialchars%3f%26%3cz%3e!%5btest%5d%2b">ASP Page A - manually encoded url</uc:asplink>
    <br />
    <uc:asplink ID="Asplink1" runat="server" >ASP Page B - URL encoded in codebehind</uc:asplink>
    <br />
    </fieldset>
    <br />
    <fieldset>
    <legend>For comparison, same links as static HTML</legend>
    <a href="/webtest/asppage.asp?param1=foo&param2=specialchars%3f%26%3cz%3e!%5btest%5d%2b">Plain HTML Href - Direct</a>
    <br />
    <a href="http://demo/webtest/sessionupdate.aspx?url=/webtest/asppage.asp?param1=foo&param2=specialchars%3f%26%3cz%3e!%5btest%5d%2b">Plain HTML Href - Through intermediate session page</a>
    <br />
    </fieldset>

 

Manually setting a property in code behind for ASP Page B:

protected void Page_Init(object sender, EventArgs e)
{
    Asplink1.href = "/webtest/asppage.asp?param1=foo&param2=" + Server.UrlEncode("specialchars?&<z>![test]+");
}

 

asplink server control:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace arkelibrary.controls
{
    /// <summary>
    /// This control takes a href parameter and renders the page
    /// with a link to 'localclicktracker' and that href appended,
    /// plus javascript to swap the localclicktracker href out to the direct
    /// link and more javascript to swap it back in mouse down.  The purpose
    /// is to have clear direct links in the user's status bar, while
    /// always sending users through the clicktracker intermediate page regardless
    /// of whether they have javascript enabled or not.
    /// e.g.: 
    /// <uc:asplink runat="server" href="/dir/target.asp">ASP Page</uc:asplink>
    /// </summary>
    [DefaultProperty("Text")]
    [ToolboxData("<{0}:asplink runat=server Target='{1}'></{0}:asplink>")]
    [ParseChildren(true, "Text")]
    public class asplink : ArkeControlBase
    {
        private const string localclicktracker = "/webtest/sessionupdate.aspx?url=";

        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Localizable(true)]
        public string href
        {
            get
            {
                String s = (String)ViewState["href"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {
                ViewState["href"] = value;
            }
        }

        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Localizable(true)]
        public string Text
        {
            get
            {
                String s = (String)ViewState["Text"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {
                ViewState["Text"] = value;
            }
        }

        protected override HtmlTextWriterTag TagKey
        {
            get
            {
                return HtmlTextWriterTag.A;
            }
        }

        protected override void AddControlAttributes()
        {
            AddUnencodedAttribute(HtmlTextWriterAttribute.Href, localclicktracker + href);
            base.Attributes.Add(HtmlTextWriterAttribute.Id.ToString(), this.ClientID);
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            Type type = GetType();
            if (!this.Page.ClientScript.IsClientScriptBlockRegistered(type, "friendlyhref"))
            {
                string script = @"<script type=""text/javascript"">
window.rewrite=function(t){
  try {
    var orig=t.href;
    var esc=encodeURIComponent||escape;
    if (t.href.indexOf(""" + localclicktracker + @""") < 0) {
      t.href=""" + localclicktracker + @""" + esc(t.href)
    }
    t.onmousedown="""";
  } catch (err) {try {t.href=orig;} catch (e2) {}}
  return true;
}
window.friendlyhref=function(id,target){ 
  try {
    var t=document.getElementById(id);
    if (t) {
      var orig=t.href;
      t.href=target;
      t.onmousedown=function() {return rewrite(t);};
    }
  } catch (err) {try {t.href=orig;t.onmousedown="""";} catch (e2) {}}
}
</script>";
                this.Page.ClientScript.RegisterClientScriptBlock(type, "friendlyhref", script);
            }
        }

        protected override void Render(HtmlTextWriter writer)
        {
            AddAttributesToRender(writer);
            writer.RenderBeginTag(HtmlTextWriterTag.A);
            if (Text == String.Empty)
            {
                Text = href;
            }
            writer.WriteEncodedText(Text);
            writer.RenderEndTag();

            // Render is too late in the page processing cycle for Page.ClientScript.RegisterClientScriptBlock
            // to work.  I could instead use a startup script registered in onprerender, but that 
            // would wait until the whole page was loaded before fixing up the links.  So
            // instead I just write javascript here directly so that it will run as soon as possible.
            string scriptForThis = String.Format(@"<script type=""text/javascript"">friendlyhref('{0}','{1}');</script>",
                this.ClientID, href);
            writer.WriteLine(scriptForThis);
        }
    }
}

 

Base class:

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace arkelibrary.controls
{
    /// <summary>
    /// WebControl by default will encode any attributes
    /// added to it.  We want to add two features:
    /// 1) Support already-encoded attributes
    /// 2) Support asserting that our attributes don't include any <%= %> blocks, which
    ///    show up during conversion from ASP and are easy to miss.
    /// </summary>
    public abstract class ArkeControlBase : WebControl
    {
        // we have to support two separate attribute collections, because
        // the base class collection encodes everything
        private Dictionary<HtmlTextWriterAttribute, string> unencodedAttrs = new Dictionary<HtmlTextWriterAttribute, string>();

        // code blocks can show up when translating from asp.
        // they are dangerous because they will work fine if in a plain html control,
        // but will fail if in a server control.  So, error out if we hit one.
        protected void AssertNoCodeBlocks()
        {
            foreach (var key in this.Attributes.Keys)
            {
                AssertNoCodeBlock(key, this.Attributes[key.ToString()]);
            }
            foreach (var pair in unencodedAttrs)
            {
                AssertNoCodeBlock(pair.Key, pair.Value);
            }
        }

        protected void AssertNoCodeBlock(object key, string value)
        {
            if (value.Contains("<%"))
            {
                throw new ArgumentException("Can not have asp.net block tag in control attribute: " + key + "=" + value);
            }
        }

        // override this method in subclasses and add attributes -
        // add to base.Attributes.Add(key,value) anything that should be encoded,
        // and unencodedAttrs[key]=value anything that should not be encoded.
        protected abstract void AddControlAttributes();

        protected void AddUnencodedAttribute(HtmlTextWriterAttribute key, string value)
        {
            unencodedAttrs[key]=value;
        }

        // calls our internal addcontrolattributes method, asserts we have no code blocks,
        // adds base attributes to writer, then adds our unencoded attributes to writer.
        protected override void AddAttributesToRender(
            HtmlTextWriter writer)
        {
            // add attributes to control
            AddControlAttributes();

            // validate attributes
            AssertNoCodeBlocks();

            // copy control attributes to writer
            base.AddAttributesToRender(writer);
            foreach (var pair in unencodedAttrs)
            {
                writer.AddAttribute(pair.Key, pair.Value);
            }
        }
    }
}

Posted by David Eison on Friday, September 3, 2010 7:35 PM
Permalink | Comments (0) | Post RSSRSS comment feed