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.
Thanks for sharing!