Screen capture of the design window
Screen capture as it appears in a browser
Introduction
One of my programs had to visualize data that could not be returned by DB queries. I used a Table
to display it. Later, the client wanted to be able to click on a cell to mark it, which had to be stored in the database. A TableCell
doesn't have a server-side click-event, so I had to find another way.
The solution I was looking had to meet the following requirements:
- It had to fire a server-side event that also provided the
TableCell
that was clicked.
- It must also work if I was to add an AutoPostBack control in the future, without having to modify the code.
(Note: The solution proposed in this article can be used with any HTML element, not just TableCell
s).
What I tried, but didn't work
My first attempt had a form with:
- Three HTML elements, which the user must click
- A label to display the HTML element that was clicked
- A hidden field to pass a value that would indicate which HTML element was clicked, and
- A Submit button
To the three HTML elements, I added JavaScript code that would write a value in the hidden field and then simulate clicking the Submit button. This worked, but had one drawback: the Submit button had to be visible. If I made it invisible, a JavaScript error occurred, because ASP.NET had not included the invisible Submit button in the generated HTML and therefore it could not be found by the JavaScript code.
I then read the article Clickable Rows in a DataGrid by Dave Hurt. He used the __doPostBack
function that is generated by ASP.NET to detect what DataGrid
row was clicked. The idea was sound, but it only works if the webpage does contain a visible AutoPostBack control (TextBox
, CheckBox
or DropDownList
with the AutoPostBack
property set to true). I did not have, nor want, a visible AutoPostBack control on my page.
How postback works
Before I show you what I tried and did work, it is important to know how AutoPostBack works. Have a look at the HTML code that is generated when there is an AutoPostBack control:
<form name="Form1" method="post" action="WebForm1.aspx" id="Form1">
<input type="hidden" name="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" value="dDwtODM1(...)+sg==" />
<script language="javascript" type="text/javascript">
<!--
function __doPostBack(eventTarget, eventArgument) {
var theform;
if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) {
theform = document.Form1;
}
else {
theform = document.forms["Form1"];
}
theform.__EVENTTARGET.value = eventTarget.split("$").join(":");
theform.__EVENTARGUMENT.value = eventArgument;
theform.submit();
}
</script>
(...)
<input name="TextBox1" type="text"
onchange="__doPostBack('TextBox1','')"
language="javascript" id="TextBox1" /></P>
</form>
A few things are important to notice:
- ASP.NET added two hidden
input
controls: __EVENTTARGET
and __EVENTARGUMENT
.
- It added a JavaScript function
__doPostBack
.
- It added JavaScript to handle the client-side
onChange
event, which calls __doPostBack
.
The basic idea
The answer to our problem has to both mimic this behavior and make it possible to mimic this behavior.
Postback uses the __doPostBack
function and the hidden INPUT
's __EVENTTARGET
and __EVENTARGUMENTS
. These are only generated if the WebForm contains an AutoPostBack control. If it does not, the __doPostBack
method and the two hidden INPUT
s must be created.
To know if elements are clicked, the WebForm must contain a button. The clickable HTML elements must do a postback as if that button was clicked. We can then handle that button's Click
event on the server. To know *which* HTML element was clicked, we add a server-side hidden INPUT
(runat="server"
) to our WebForm. When an HTML element is clicked, it can write a value that uniquely identifies it to this hidden INPUT
. When we handle that button's Click
event, the value of the hidden INPUT
identifies the HTML element that was clicked.
Solution
The WebForm must contain:
- A JavaScript function
elementClick(value)
, where value
is a string that uniquely identifies the HTML element that was clicked. This function will write value
to myHidden
(see below) and mimic clicking btnSubmit
(see below).
- Three HTML elements of which we want to know if it is clicked. Each element must call
elementClick
with a unique value when it is clicked (client side, that is). In my example, the HTML elements are TableCell
s with the texts "One", "Two", and "Three". They will call elementClick
with the values "1", "2", and "3".
- An
ASP:Label
, called "lblResult
", to show the HTML element that was clicked.
- A server-side hidden
INPUT
(runat="server"
), called "myHidden
", which we will use to pass the value to the server.
- A hidden
ASP:Button
, called "btnSubmit
", to make it possible to capture a server-side event.
VB.NET: HasPostBacks(Form)
As said before, the hidden INPUT
's __EVENTTARGET
and __EVENTARGUMENT
must be created if there is no AutoPostBack element in the WebForm. HasPostBacks
checks a WebForm to see if it can find any control that has a AutoPostBack
property set to True. It iterates through all the controls in the form, and it stops if it finds a control which posts back automatically.
The frameWork contains three controls that have an AutoPostBack
property: CheckBox
, TextBox
, DropDownList
. However, you could have created your own control that does post back automatically, and it needn't be a CheckBox
, TextBox
, or DropDownList
. So checking if the control is a CheckBox
, TextBox
, or DropDownlist
, and then checking if the AutoPostBack
property is set, will not be a robust way of checking. I thought of only one thing when my solution will not work: when a User Control does post back automatically, but does not expose the AutoPostBack
property. In order to catch all the controls that have the AutoPostBack
property, I simply query the property. If it generates an error because that control does not have that property, obviously I does not post back automatically.
Protected Function HasPostBacks(ByVal Frm As HtmlForm) As Boolean
Dim Ctrls As ControlCollection = Frm.Controls
Dim i As Integer = 0
Dim Found As Boolean = False
While i < Ctrls.Count And Not Found
Dim Ctrl As Object = Ctrls(i)
Try
Found = Ctrl.AutoPostBack
Catch Ex As Exception
Found = False
End Try
i += 1
End While
Return Found
End Function
VB.NET: EnsurePostBack()
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
EnsurePostBack()
If Not Me.IsPostBack Then
Table1.Rows(0).Cells(0).Attributes.Add("onClick", "elementClick('1')")
Table1.Rows(0).Cells(1).Attributes.Add("onClick", "elementClick('2')")
Table1.Rows(0).Cells(2).Attributes.Add("onClick", "elementClick('3')")
TextBox1.Text = IIf(TextBox1.AutoPostBack, "PostBack = True", "PostBack = False")
End If
End Sub
EnsurePostBack
will add the hidden INPUT
s __EVENTTARGET
and __EVENTARGUMENTS
if there are no AutoPostBack controls. First, it tries to find the Form
object. With it, it uses HasPostBacks
to determine if the hidden INPUT
s must be added. If yes, it creates two HtmlInputHidden
objects, sets their properties, and adds them to the Form
.
Protected Sub EnsurePostBack()
Dim Frm As Control = Me.FindControl("Form1")
Dim HPB As Boolean = HasPostBacks(Frm)
If Not HPB Then
Dim EventTarget As HtmlInputHidden = New HtmlInputHidden()
Dim EventArguments As HtmlInputHidden = New HtmlInputHidden()
EventTarget.ID = "__EVENTTARGET"
EventTarget.Name = "__EVENTTARGET"
EventArguments.ID = "__EVENTARGUMENT"
EventArguments.Name = "__EVENTARGUMENT"
Frm.Controls.Add(EventTarget)
Frm.Controls.Add(EventArguments)
End If
End Sub
VB.NET: Page_Load
Every time the page loads, EnsurePostBack
must be called. Because the controls are not part of the WebForm, they are not automatically recreated as controls that are part of the WebForm are.
In my case, I had a Table
where I wanted to know which cells were clicked. So in my example, I have a Table
with one TableRow
with three TableCell
s. Because the designer doesn't allow to add attributes to a TableCell
, I added the attributes in the Page.Load
event. Because the TableCell
s are initially a part of the WebForm, the TableCell
s don't have to be added to the WebForm each time it is loaded. And because the TableCell
s are persistent, so are attributes of the TableCell
s. The attributes have to be added only once. "If Not Me.IsPostBack Then
" makes sure it does happen only once.
The attribute names are "onClick
", because we want to capture the client-side onClick
event. The value of the arguments are a call to the JavaScript function elementClick
, with a value of "1", "2", or "3".
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
EnsurePostBack()
If Not Me.IsPostBack Then
Table1.Rows(0).Cells(0).Attributes.Add("onClick", "elementClick('1')")
Table1.Rows(0).Cells(1).Attributes.Add("onClick", "elementClick('2')")
Table1.Rows(0).Cells(2).Attributes.Add("onClick", "elementClick('3')")
TextBox1.Text = IIf(TextBox1.AutoPostBack, "PostBack = True", "PostBack = False")
End If
End Sub
JavaScript: myPostBack(eventTarget)
It is only possible to do a postback if there is a __doPostBack
method. This is only generated if the WebForm contains an AutoPostBack control. To do a postback anyway, there must be a function that does the same thing as __doPostBack
, but has a different name. I call it myPostBack
. Because it will never post back event arguments, I changed the code to:
function myPostBack(eventTarget) {
var eventArgument;
eventArgument = '';
var theform;
if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) {
theform = document.Form1;
}
else {
theform = document.forms["Form1"];
}
theform.__EVENTTARGET.value = eventTarget.split("$").join(":");
theform.__EVENTARGUMENT.value = eventArgument;
theform.submit();
}
JavaScript: elementClick(value)
This method will be called by the clickable HTML elements. I use Microsoft's way of getting the form. If I have the form, I can get to the myHidden
control, which will be assigned the value that was passed to this method. (Of course, it is also possible to use the document.getItemByID
method, but I think this is neater.) Then, myPostBack
will be called with the event-target 'btnSubmit
', so that on the server-side, the btnSubmit.Click
event will be fired.
function elementClick(value) {
var theform;
if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) {
theform = document.Form1;
}
else {
theform = document.forms["Form1"];
}
var myHddn;
myHddn = theform.myHidden;
myHddn.value = value;
myPostBack('btnSubmit');
}
Final words
I'd like to thank Dave Hurt for his article on Clickable Rows in a DataGrid. It helped me to find a solution for my problem. I hope my example can help you find your solution.
(If you want to try the example, start Visual Studio and create a new project called "HTMLElementClick". Download the source code and extract it to the project directory, overwriting the files Visual Studio created.)