To read the article online, visit http://www.4GuysFromRolla.com/articles/082405-1.aspx

Stopping JavaScript Errors When Opening a LinkButton in a New Window

By Scott Mitchell


Introduction


A previous article of mine, Displaying Text in the Browser's Status Bar When Mousing Over a LinkButton, examined how to enhance the LinkButton control to display custom text in the browser's status bar when the link was moused over. Specifically, the article illustrated how to build a custom, compiled Web control that extended the System.Web.UI.WebControls.LinkButton class, providing extra properties and overriding existing LinkButton methods in order to tack on extra functionality to the existing LinkButton control. (The article also looked at how to add properties to the enhanced LinkButton control to display a client-side confirm messagebox when clicked; this messagebox had OK and Cancel buttons and only continued with the postback if the user clicked the OK button.)

I recently received an email inquiry in my Inbox from loyal 4Guys reader David P., who asked:

I'm writing to you because I think you may be able to help me. I enjoyed your article about overriding Linkbutton very much, but I have a further need that you may have a solution to. A LinkButton is rendered like <a href="javascript:__doPostBack(.....)" onClick="alert('pressed')">Click me</a>.

My problem is that if I SHIFT+CLICK the LinkButton [or right-click on it and opt to open in a new window], a new window opens [with the JavaScript, javascript:__doPostBack(.....), in the Address bar, resulting in a client-script error and a confused user.] ... I want to avoid that. I've found a solution:

<a href='#' onClick="__doPostBack(....);">Click me</a>

My problem is that I don't know which attribute to modify so that the href value is moved to the onClick event... It must be done in an inherited control or codebehind.

(To see an example of David's problem, click here. The hyperlink's markup is simply <a href="javascript:var x = 4;" target="_blank">click here</a>. Note that clicking the link opens a new window that displays the JavaScript in the address bar and returns an error. This mimics the behavior of an ASP.NET LinkButton that is clicked to have opened in a new window.)

Good question, David! There is a way to "rearrange" the values of the rendered LinkButton's attributes, but it isn't as simple or straightfoward as one might hope. In this article we'll add additional code to the skmLinkButton control examined previously in Displaying Text in the Browser's Status Bar When Mousing Over a LinkButton so that it emits an href="#" and the proper script in the client-side onclick event. Read on to learn more!

Examining How the LinkButton Class Renders Its Href Attribute


In order to properly extend the built-in LinkButton Web control in order to meet our objectives, it's important that we have a solid understanding as to how the LinkButton is rendered. If you use a tool like Reflector you can peer into the source code of the LinkButton class. All of the interesting work takes place in the AddAttributesToRender(HtmlTextWriter) method. This method, which is common to all Web controls, is responsible for adding the attributes to the rendered HTML markup (in the case of the LinkButton, the HTML element rendered is <a>). The HtmlTextWriter instance that's passed into this method is the object to which the rendered element's attributes are written.

The code, in C#, for the LinkButton's AddAttributesToRender(HtmlTextWriter) method is as follows:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
   if (this.Page != null)
   {
      this.Page.VerifyRenderingInServerForm(this);
   }
   base.AddAttributesToRender(writer);
   if (this.Enabled && (this.Page != null))
   {
      if (this.CausesValidation && (this.Page.Validators.Count > 0))
      {
         writer.AddAttribute(HtmlTextWriterAttribute.Href, 
               "javascript:" + Util.GetClientValidatedPostback(this));
      }
      else
      {
         writer.AddAttribute(HtmlTextWriterAttribute.Href, 
                       this.Page.GetPostBackClientHyperlink(this, ""));
      }
   }
}

The first thing the method does is check to make sure that the LinkButton is being rendered within a Web Form (i.e., a <form runat="server">). Following that, the base class's AddAttributesToRender(HtmlTextWriter) method is called. This base class method does a number of things, including adding formatting information. (For example, if you set the LinkButton's ForeColor property to Red, the base class's AddAttributesToRender(HtmlTextWriter) method will inject in the rendered HTML element the attribute: style="color:red;".)

Next, if the control is enabled, the Href attribute is assigned the appropriate value. If the LinkButton's CausesValidation property is True and there are validation controls on the page, the script emited to the Href attribute includes a check to only postback if all validators on the page that are using client-side script report that their data is valid. (That's what the internal Util.GetClientValidatedPostback(this) method does.) If there is no validation controls on the page or if CausesValidation is False, the JavaScript assigned to the Href attribute is simply the call to the client-side __doPostBack(...) function. (The __doPostBack(...) function is automatically added by the ASP.NET Page class when using a Web Form. This JavaScript function causes the form to be posted back, just like had a standard Submit button been clicked.)

Moving the Href Attribute Value to the Onclick Attribute


As aforementioned, the problem with having the JavaScript call to __doPostBack(...) in the Href attribute is that opening the link in a new window will produce undesireable effects. What we'd rather do, is have the Href element have a value of # and have the client-side script that's currently in the Href attribute moved to the Onclick attribute. If we could just go in and tinker with the LinkButton class's AddAttributesToRender(HtmlTextWriter) method we'd simply add writer.AddAttribute(HtmlTextWriterAttribute.Href, "#"); and change the existing AddAttribute() lines to specify as their first parameter HtmlTextWriterAttribute.Onclick. However, we can't directly modify this method - we can only extend it.

Let's take the skmLinkButton class that was examined in an earlier 4Guys article, Displaying Text in the Browser's Status Bar When Mousing Over a LinkButton. If you'll take a moment to check out that class, you'll see that it's AddAttributesToRender(HtmlTextWriter) method has the following code:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
  base.AddAttributesToRender (writer);

  if (ShowConfirm)
    writer.AddAttribute("onclick", 
         string.Concat("javascript:return confirm('", 
                    EscapeJavaScript(ConfirmMessage), "');"));
            
  if (ShowStatusBarText)
  {
    writer.AddAttribute("onmouseover", 
         string.Concat("javascript:window.status='", 
                    EscapeJavaScript(StatusBarText), "';return true;"));
    writer.AddAttribute("onmouseout", 
                    "javascript:window.status='';return true;");
  }
}

Recall that skmLinkButton adds two additional features to the built-in LinkButton control:

  1. The ability to display text in the status bar when the user mouses over the link (this functionality is added via the second conditional statement), and
  2. The ability to add a client-side confirm dialog box. This logic is handled in the first conditional.
At first guess we may think that we can simply update skmLinkButton to handle the new requirements by adjusting the AddAttributesToRender(HtmlTextWriter) method to the following:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
  base.AddAttributesToRender (writer);

  writer.AddAttribute(HtmlTextWriterAttribute.Href, "#");

  if (this.Enabled && (this.Page != null))
  {
     if (this.CausesValidation && (this.Page.Validators.Count > 0))
     {
        writer.AddAttribute(HtmlTextWriterAttribute.Href, 
              "javascript:" + Util.GetClientValidatedPostback(this));
     }
     else
     {
        writer.AddAttribute(HtmlTextWriterAttribute.Href, 
                      this.Page.GetPostBackClientHyperlink(this, ""));
     }
  }

  if (ShowConfirm)
    writer.AddAttribute("onclick", 
         string.Concat("javascript:return confirm('", 
                    EscapeJavaScript(ConfirmMessage), "');"));
            
  if (ShowStatusBarText)
  {
    writer.AddAttribute("onmouseover", 
         string.Concat("javascript:window.status='", 
                    EscapeJavaScript(StatusBarText), "';return true;"));
    writer.AddAttribute("onmouseout", 
                    "javascript:window.status='';return true;");
  }
}

There are two problems with the above, proposed additions. First, the Util.GetClientValidatedPostback() method is internal to the System.Web.dll assembly, meaning that we cannot create our own class that calls it. Thankfully, we have Reflector handy, so we can merely copy the code from this method to the skmLinkButton class (it's a mere couple of lines of code in total). In fact, we'll need to tweak the code for the Util.GetClientValidatedPostback() method just a bit, as we'll see momentarily.

The other challenge is that using the above code, the skmLinkButton will still emit its JavaScript in the Href attribute. This is because the base class's AddAttributesToRender(HtmlTextWriter) method adds this Href value first (as we're calling base.AddAttributesToRender(writer) before doing our own writer.AddAttribute(HtmlTextWriterAttribute.Href, "#");. When rendering the <a> element, the HtmlTextWriter will use the first attribute value (the script emitted from the base class's AddAttributesToRender(HtmlTextWriter) method) rather than the second value (the # added from the skmLinkButton's AddAttributesToRender(HtmlTextWriter) method). The solution is easy enough - call the base class's AddAttributesToRender(HtmlTextWriter) method after we've already set the Href attribute from skmLinkButton. (Personally this feels like a serious hack, but I do not see any other way around, as the HtmlTextWriter class doesn't have any methods to remove existing attribute values...)

The final challenge is making sure that the script for the client-side confirm (if present) is also included in the Onclick attribute in the right place. These challenges are all resolved by using the following code:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
  writer.AddAttribute(HtmlTextWriterAttribute.Href, "#");

  string onClickScript = "javascript:";
  string confirmScript = string.Empty;
      
  if (this.ShowConfirm)
    confirmScript = string.Concat("if (confirm('", 
                           EscapeJavaScript(ConfirmMessage), "')) ");

  if (this.Enabled && (this.Page != null))
  {
    if (this.CausesValidation && (this.Page.Validators.Count > 0))
    {
      onClickScript += GetClientValidatedPostback(this, confirmScript);
    }
    else
    {
      onClickScript +=  string.Concat(confirmScript, 
                        this.Page.GetPostBackEventReference(this, ""));
    }

    writer.AddAttribute(HtmlTextWriterAttribute.Onclick, onClickScript);
  }

    
  if (ShowStatusBarText)
  {
    writer.AddAttribute("onmouseover", 
                   string.Concat("javascript:window.status='", 
                   EscapeJavaScript(StatusBarText), "';return true;"));
    writer.AddAttribute("onmouseout", 
                   "javascript:window.status='';return true;");
  }

  base.AddAttributesToRender(writer);
}

protected virtual string GetClientValidatedPostback(Control control, 
                                                    string confirmScript)
{
  string text1 = control.Page.GetPostBackEventReference(control);
  return ("{if (typeof(Page_ClientValidate) != 'function' ||  " +
                "Page_ClientValidate()) " + confirmScript + text1 + "} ");
}

One small, but important, change worth noting: previously if there were no validation controls on the page or if the LinkButton's CausesValidation property was false, the script injected into the Href attribute was returned by a call to Page.GetPostBackClientHyperlink(this, ""). The problem with this method is that it prepends the javascript: markup to the call to __doPostBack(...). This works fine if we are not adding our own call to confirm(), but if we are, the created script will look something like: javascript:if confirm('...') javascript:__doPostBack(...). Note the second, superfluous javascript: in there, preceeding the call to __doPostBack(...). IE doesn't seem to mind this, but FireFox, for example, raises a client-side script error.

To remedy this, we can replace the call to Page.GetPostBackClientHyperlink(this, "") with Page.GetPostBackEventReference(this, ""), which simply returns the proper call to __doPostBack(...) without prepending javascript:.

Using skmLinkButton in an ASP.NET Page


The previous article, Displaying Text in the Browser's Status Bar When Mousing Over a LinkButton, discusses how to use skmLinkButton in an ASP.NET page. With this addition, there are no code changes necessary for using skmLinkButton.

A live demo of the new, enhanced skmLinkButton is available at http://scottonwriting.net/demos/skmLinkButtonDemo.aspx. Note that when SHIFT+clicking on a LinkButton in the live demo, or when right-clicking on the LinkButton and choosing to open the link in a new window, the resulting new window simply reloads the existing page. It doesn't cause a JavaScript error or show a blank page, as is the case with the built-in LinkButton.

Happy Programming!

  • By Scott Mitchell


    Attachments:

  • View a live demo
  • Download the complete source code and a pre-compiled assembly
  • Article Information
    Article Title: ASP.NET.Stopping JavaScript Errors When Opening a LinkButton in a New Window
    Article Author: Scott Mitchell
    Published Date: August 24, 2005
    Article URL: http://www.4GuysFromRolla.com/articles/082405-1.aspx


    Copyright 2017 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers