November Happy Hour will be moved to Thursday December 5th.

Magnus Rahl
Mar 20, 2009
  6727
(0 votes)

The accessible LinkButtton

The ASP.NET LinkButton webcontrol is great in many ways, but one clear shortcoming i accessibility. Because it doesn’t render a xhtml input control it can not perform http POST on it’s own, it uses javascript to do this. If, for accessibility reasons or other, javascript is disabled/unavailable the LinkButton won’t work.

 

The idea

What if we were to output both a LinkButton and a Button (which renders an html input control capable of http POST), with identical behaviour, and let the client decide which one to display depending on the availability of javascript? It would make design difficult for sure, but at least we can produce the design we like when javascript is available (LinkButton displayed) while providing a functionally adequate alternative when it’s not (Button displayed).

 

A solution

I could have extended the LinkButton class, but for simplicity (not having to poke around in the render methods etc.) I chose to create a container extending the Control class and put the LinkButton and button inside. The LinkButton has an element style of ‘display:none’ when delivered to the client, which is changed by javascript to ‘display:inline’ (inline can easily be changed if you want something else). The noscript section of the javascript contains the Button, so if the script doesn’t run it’s visible instead. Simple, isn’t it?

Code:

   1: using System;
   2: using System.Web;
   3: using System.Web.UI;
   4: using System.Web.UI.WebControls;
   5:  
   6: namespace Sogeti.Controls
   7: {
   8:     /// <summary>
   9:     /// Alternative to LinkButton which is more accessible in that it will
  10:     /// fall back to a Button, which does not require javascript for
  11:     /// postbacks, if client does not support javascript.
  12:     /// Composite control consisting of a LinkButton and a Button, where
  13:     /// the LinkButton will be hidden at render time (element style display
  14:     /// set to "none") and made visible again by javascript in the client
  15:     /// (element display set to the value stored in LinkVisibleDisplayStyle).
  16:     /// The noscript section instead displays the Button (while the
  17:     /// LinkButton remains hidden.
  18:     /// The public properties (like CommandArgument and CssClass) affect both
  19:     /// the LinkButton and the Button. The events (Click and Command) reflect
  20:     /// the corresponding events of both the Button and the LinkButton.
  21:     /// </summary>
  22:     public class AccessibleLinkButton : Control
  23:     {
  24:         #region Private members
  25:  
  26:         private string _linkVisibleDisplayStyle;        
  27:  
  28:         private LinkButton _link;
  29:         private Button _button;
  30:         private Literal _scriptContainer;
  31:  
  32:         #endregion
  33:  
  34:         #region Public events
  35:  
  36:         /// <summary>
  37:         /// Raised whenever the Command event of the contained Linkbutton or
  38:         /// Button is raised, forwarding the CommandEventArgs.
  39:         /// </summary>
  40:         public event CommandEventHandler Command;
  41:  
  42:         /// <summary>
  43:         /// Raised whenever the Click event of the contained Linkbutton or
  44:         /// Button is raised, forwarding the EventArgs
  45:         /// </summary>
  46:         public event EventHandler Click;
  47:  
  48:         #endregion
  49:  
  50:         #region Constructors
  51:  
  52:         public AccessibleLinkButton()
  53:         {
  54:             // Default value
  55:             this.LinkVisibleDisplayStyle = "inline";
  56:  
  57:             // Create members
  58:             this.Link = new LinkButton();
  59:             this.Button = new Button();
  60:             this.ScriptContainer = new Literal();
  61:  
  62:             // Set Link to hidden as default
  63:             this.Link.Attributes.CssStyle["display"] = "none";
  64:  
  65:             // Register event proxies
  66:             this.Link.Click += new EventHandler(EventProxy_Click);
  67:             this.Button.Click += new EventHandler(EventProxy_Click);
  68:             this.Link.Command += new CommandEventHandler(EventProxy_Command);
  69:             this.Button.Command += new CommandEventHandler(EventProxy_Command);
  70:  
  71:             // Build control tree
  72:             Literal html; // Used to inject html
  73:  
  74:             this.Controls.Add(this.Link);
  75:  
  76:             html = new Literal();
  77:             html.Text = "<script type=\"text/javascript\" language=\"javascript\">";
  78:             this.Controls.Add(html);
  79:  
  80:             this.Controls.Add(this.ScriptContainer); // Contents of ScriptContainer is set later
  81:  
  82:             html = new Literal();
  83:             html.Text = "</script><noscript>";
  84:             this.Controls.Add(html);
  85:  
  86:             this.Controls.Add(this.Button);
  87:  
  88:             html = new Literal();
  89:             html.Text = "</noscript>";
  90:             this.Controls.Add(html);
  91:         }
  92:  
  93:         #endregion
  94:  
  95:         #region Public properties
  96:  
  97:         public string AccessKey
  98:         {
  99:             get
 100:             {
 101:                 return this.Link.AccessKey;
 102:             }
 103:             set
 104:             {
 105:                 this.Button.AccessKey = this.Link.AccessKey = value;
 106:             }
 107:         }
 108:  
 109:         public bool CausesValidation
 110:         {
 111:             get
 112:             {
 113:                 return this.Link.CausesValidation;
 114:             }
 115:             set
 116:             {
 117:                 this.Button.CausesValidation = this.Link.CausesValidation = value;
 118:             }
 119:         }
 120:  
 121:         public string CommandArgument
 122:         {
 123:             get
 124:             {
 125:                 return this.Link.CommandArgument;
 126:             }
 127:             set
 128:             {
 129:                 this.Button.CommandArgument = this.Link.CommandArgument = value;
 130:             }
 131:         }
 132:  
 133:         public string CommandName
 134:         {
 135:             get
 136:             {
 137:                 return this.Link.CommandName;
 138:             }
 139:             set
 140:             {
 141:                 this.Button.CommandName = this.Link.CommandName = value;
 142:             }
 143:         }
 144:  
 145:         public string CssClass
 146:         {
 147:             get { return Link.CssClass; }
 148:             set { Button.CssClass = Link.CssClass = value; }
 149:         }
 150:  
 151:         public bool Enabled
 152:         {
 153:             get
 154:             {
 155:                 return this.Link.Enabled;
 156:             }
 157:             set
 158:             {
 159:                 this.Button.Enabled = this.Link.Enabled = value;
 160:             }
 161:         }
 162:  
 163:         /// <summary>
 164:         /// The value of the display attribute in the linkbutton when it is
 165:         /// visible (when hidden, display="none"), defaults to "inline"
 166:         /// </summary>
 167:         public string LinkVisibleDisplayStyle
 168:         {
 169:             get { return _linkVisibleDisplayStyle; }
 170:             set { _linkVisibleDisplayStyle = value; }
 171:         }
 172:  
 173:         public string OnClientClick
 174:         {
 175:             get
 176:             {
 177:                 return this.Link.OnClientClick;
 178:             }
 179:             set
 180:             {
 181:                 this.Button.OnClientClick = this.Link.OnClientClick = value;
 182:             }
 183:         }
 184:  
 185:         public string PostBackUrl
 186:         {
 187:             get
 188:             {
 189:                 return this.Link.PostBackUrl;
 190:             }
 191:             set
 192:             {
 193:                 this.Button.PostBackUrl = this.Link.PostBackUrl = value;
 194:             }
 195:         }
 196:  
 197:         public string Text
 198:         {
 199:             get
 200:             {
 201:                 return Link.Text;
 202:             }
 203:             set
 204:             {
 205:                 Button.Text = Link.Text = value;
 206:             }
 207:         }
 208:  
 209:         public string ToolTip
 210:         {
 211:             get
 212:             {
 213:                 return this.Link.ToolTip;
 214:             }
 215:             set
 216:             {
 217:                 this.Button.ToolTip = this.Link.ToolTip = value;
 218:             }
 219:         }
 220:  
 221:         public short TabIndex
 222:         {
 223:             get
 224:             {
 225:                 return this.Link.TabIndex;
 226:             }
 227:             set
 228:             {
 229:                 this.Button.TabIndex = this.Link.TabIndex = value;
 230:             }
 231:         }
 232:  
 233:         public string ValidationGroup
 234:         {
 235:             get
 236:             {
 237:                 return this.Link.ValidationGroup;
 238:             }
 239:             set
 240:             {
 241:                 this.Button.ValidationGroup = this.Link.ValidationGroup = value;
 242:             }
 243:         }
 244:  
 245:         #endregion
 246:  
 247:         #region Protected accessors
 248:  
 249:         protected LinkButton Link
 250:         {
 251:             get { return _link; }
 252:             set { _link = value; }
 253:         }
 254:  
 255:         protected Button Button
 256:         {
 257:             get { return _button; }
 258:             set { _button = value; }
 259:         }
 260:  
 261:         protected Literal ScriptContainer
 262:         {
 263:             get { return _scriptContainer; }
 264:             set { _scriptContainer = value; }
 265:         }
 266:  
 267:         #endregion
 268:  
 269:         #region Event handlers
 270:  
 271:         protected override void OnPreRender(EventArgs e)
 272:         {
 273:             base.OnPreRender(e);
 274:  
 275:             // Insert the script which shows the linkbutton
 276:             string script = "var obj = document.getElementById('{0}'); obj.style.display='{1}';";
 277:             this.ScriptContainer.Text = String.Format(script, Link.ClientID, LinkVisibleDisplayStyle);
 278:         }
 279:  
 280:         protected void EventProxy_Command(object sender, CommandEventArgs e)
 281:         {
 282:             if (this.Command != null)
 283:             {
 284:                 // Forward event
 285:                 this.Command(this, e);
 286:             }
 287:         }
 288:  
 289:         protected void EventProxy_Click(object sender, EventArgs e)
 290:         {
 291:             if (this.Click != null)
 292:             {
 293:                 // Forward event
 294:                 this.Click(this, e);
 295:             }
 296:         }
 297:  
 298:         #endregion
 299:     }
 300: }

As you can see, the Click and Command events are available, so are most properties for CssClass, CommandName etc.

 

Known limitations

The AccessibleLinkButton can not have child elements like the normal LinkButton. If you try to put elements in it will end up next to the LinkButton/Button rendered. This is because the Button control does not support child elements, so there was no natural way to handle it. I thought it was better to leave it like this, which at least clearly shows the limitation, than to put the child element in the LinkButton and do nothing with the Button. If you want to use a text, use the Text property. If you want to use an image, use the ImageButton which can handle POST itself.

Mar 20, 2009

Comments

Sep 21, 2010 10:32 AM

Thanks for sharing!

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog