Dr. GUI .NET #8

 

September 20, 2002

Summary: The good doctor follows up on his work on Conway's Game of Life from last month, only this time he develops the game as a Microsoft ASP.NET application, with a few strategic client-side Microsoft JScript methods. (14 printed pages)

Contents

Introduction
Where We've Been; Where We're Going
Something Old, ...
Something New, ...
Something Borrowed, and...
Something Blue!
Design Choices
Give It a Shot!
What We've Done; What's Next

Tell us and the world what you think about this article on the Dr. GUI .NET message board. And check out the samples running as Microsoft® ASP.NET applications, with source code, at: http://drgui.coldrooster.com/DrGUIdotNet/8/.

Introduction

Welcome back to Dr. GUI's ninth .NET Framework programming article. (The first eight were in honor of .NET Framework array element numbering, which starts at zero rather than one, called "Dr. GUI .NET #0" through "Dr. GUI .NET #7.") If you're looking for the earlier articles, check out the Dr. GUI .NET home page.

Dr. GUI hopes to see you participate in the message board. There are already some good discussions there, but they'll be better once you join in! Anyone can read the messages, though you'll need a Passport to authenticate you to post. Don't worry—it's easy to set up a Passport, and you can associate it with any of your e-mail accounts.

Where We've Been; Where We're Going

Last time, we developed Conway's Game of Life as a Microsoft® Windows® Forms application. You'll want to take a look at our last article to learn about what Life is and where it comes from—and because we're reusing major portions of that code.

This time we're going to convert it to a mostly server-side Microsoft® ASP.NET application—but we'll see how Dr. GUI's first serious foray into client-side JScript makes editing the playing board much faster than if we relied on server-side code.

So let's take a look at how Dr. GUI married this server-side code from last time with a new Web client. Here's what the Web application looks like:

Something Old, ...

We were able to re-use a lot of the client code from the Windows Forms application.

The biggest re-use was of the methods that calculate one Life board from another. We copied the entire LifeCalc.vb file from the old project into the new project without any modifications whatsoever.

However, we did call a different overload of the CalcNext method. In the Windows Forms version, we called the overload that takes two arrays—the old array and the new array—and we swapped the references to the arrays afterwards. In other words, we created the two arrays when we started the application, and used the same pair of arrays until the application ended. The code for calculating a new generation and swapping the arrays looks like this:

    Dim newBoard As Integer(,) = _
        LifeCalc.CalcNext(currBoard, tempBoard)
    tempBoard = currBoard
    currBoard = newBoard

In the ASP.NET version, the life cycle of our application is different: all of our variables are initialized with EACH HTTP REQUEST. This is called "stateless" operation. (Contrast with the Windows Forms version, in which we kept all sorts of state—the two arrays, the bitmap for drawing, the Graphics object, and so on—from the beginning of the application to the end.)

If we want to save state between HTTP requests, we have to do something special using some storage mechanism—a cookie, an input field (perhaps hidden) on the form, or, most flexibly, session or view state. That means that each HTTP request causes the array to be created anew—there's no good way to just keep the arrays lying around. (This stateless operation is typical of Web applications and enhances scalability.)

Since both of the arrays need to be created each time anyway in the ASP.NET version, we created the current board in Page_Load, and the new board in the overload of CalcNext, which automatically creates a result array for us and returns a reference to it. The code for that (also the handler for the "Single Step" button) looks like this:

Private Sub RunSingle_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles RunSingle.Click
    currBoard = CalcNext(currBoard)
End Sub

This sets currBoard to refer to the newly created updated board, thereby throwing away the old array (since nothing refers to it any longer). But we're about to end anyway, so there's nothing wrong with throwing away the old array a little early. (The garbage collector will eventually clean it up.)

We also used the FillRandom method unmodified. The good doctor simply copied it from the Windows Forms application to the ASP.NET application! You can see the code in the last article.

Something New, ...

In order to understand all the new stuff, including both client-side script and ASP.NET event handlers, we need to discuss the life cycle of a Web page and of an ASP.NET request so that we're clear when each bit of code is executed. (Dr. GUI is aware that this is review for many of you. Still, he found it helpful to think about as he was writing this program, since he's relatively new to Web development, so he thought some of you might find it handy.)

Life Cycle of a Web Page

What you need to know about the life cycle of a Web page is pretty simple for this application: First, the user types or clicks on the URL for the page. The browser loads the page, and it runs any script in the window_onload function, if one exists. (Our application needs one to deal with the timer, since each loading of the Web page sets all the variables anew and restarts the timer, if it's supposed to be running.)

Next, the Web page is displayed in the user's browser and waits for any user interaction. User interaction can take the form of typing or mouse events. Most events have some default action (typing causes text to appear in the currently-selected text box, and buttons cause the form to be submitted to the server, for instance), but it's possible to associate script with any event. This application has script for mouse clicks on the life board and for the timer enable/disable button, as well as an event handler to handle the timer tick.

It's possible by either a click or a call in script to generate a request to the server for a new Web page. All of our buttons except the timer enable/disable button generate server requests, and the timer tick handler generates one by calling the click method of one of the buttons—in essence, clicking it programmatically.

Note that since a timer tick indirectly generates a server request, which results in a new Web page being generated and returned to the browser, we never will get more than one timer tick on a particular instance of the page.

On the other hand, if we want to keep the timer going for more than one tick, we need a way to pass to the new Web page the information that the timer is supposed to be enabled. We'll discuss how this works below when we discuss the code on the Web page. We'll also talk about how ASP.NET handles these server requests below. (You can imagine inserting the following section, entitled "Life Cycle of an ASP.NET Request," here if you like.)

Finally, the page is unloaded—perhaps the user clicks a link, or a button, or closes the browser. You can catch the unload event if you like, but our application has no need for it, so we don't.

After that, if the user has requested a new page, the new page is loaded (it could be the same page with different form data, as in Life) and the cycle begins again.

Life Cycle of an ASP.NET Request

Once the request comes to the server, a chain of events happens. For this application, we're only concerned with a few of those events—but we are concerned with one unusual event worth noting.

The first event we care about is the page load event. All we do in the page load event handler is to initialize the array—either reading it from the hidden text field in the object or creating a new array and initializing it to a random pattern if there is no array on the Web page (for instance, when the page first comes up).

Next, if the request was because of a button click event, the appropriate event handler method (such as RunSingle_Click when the single-step button is clicked) is executed. This may modify the array.

Finally, we use the page's pre-render event to write the value of the array into a table that will be rendered onto the Web page and into a hidden INPUT element so the value can be manipulated on the client side.

ASP.NET then renders the Web page from all the controls (including our table HTML text, stored in a DIV element).

For more on the basic life cycle of a Web form, take a look at Web Forms Page Processing. And for even more detail (including about the pre-render event, take a look at Control Execution Lifecycle.

Some Things Done on the Client, Some on the Server

So, as we've mentioned, some the processing is divided between the client and the server. Let's look at the code in the order it's executed to see where each bit of processing is done.

First: Creating the Web page in response to the browser's request

The very first thing that happens in the running of this application is that the user navigates to the URL for the application.

When that happens, ASP.NET creates a page object and starts calling methods on it, including eventually our Page_Load method, which is the first code to execute. Ours looks like this:

    Private Sub Page_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        ' don't bother loading array if button clicked was Clear
        If (Request.Form.Get("Clear") = Nothing) Then
            ' wasn't Clear, so get array
            'currBoard = Session("currBoard")
            currBoard = GetBoardFromString(LifeData.Value)
            If currBoard Is Nothing Then
                currBoard = New Integer(rows - 1, cols - 1) {}
                FillRandom(200, False)
            End If
        End If
    End Sub

Here we check to see if the request took place because the Clear button was clicked. If that's the case, we're going to clear the array anyway, so there's no sense in creating it.

If the request isn't the result of clicking the Clear button (and since this is our first visit to the page, it isn't), we attempt to create the array from the hidden input element by calling GetBoardFromString. If that fails, we create a new array and fill it with random lives. (This is what will happen our first time through, since there's nothing in the hidden input element yet.)

On our first time through, none of the button click event handlers will be called—the page hasn't even been displayed to the user yet, so there's been no opportunity for the buttons to be clicked!

However, the pre-render event will be fired, allowing us to render the array into the HTML representing a table to be inserted into the DIV element in the table's proper spot. We also take this opportunity to render the array into a string to be passed to the page as a hidden element. Here's the code for handling the pre-render event and for rendering into a table and a string:

    Private Sub Page_PreRender(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.PreRender
        LifeBoard.InnerHtml = _
            RenderLifeToTable(currBoard)
        LifeData.Value = RenderLifeToString(currBoard)
        'Session("currBoard") = currBoard
    End Sub

This code sets the InnerHtml property of the LifeBoard DIV element to contain the HTML rendering of the table. (We'll show how that string is constructed in a moment.) It then renders the array into a string as well.

Here's how the array is rendered as an HTML table:

    Private Function RenderLifeToTable(ByVal board As Integer(,)) _
            As String
        Dim ret As New StringBuilder( _
                        "<table cellspacing=0 cellpadding=0>")
        Dim i, j As Integer
        For i = 0 To rows - 1
            ret.Append("<tr>")
            For j = 0 To cols - 1
                ret.AppendFormat( _
                    "<td><img id = ""IMG_{0}_{1}"" src={2}.gif></td>", _
                    i, j, IIf(currBoard(i, j) = 0, "empty", "alive"))
            Next
            ret.Append("</tr>")
        Next
        ret.Append("</table>")
        Return ret.ToString()
    End Function

First, we create a StringBuilder, not a String, to hold the resulting HTML. We initialize it to be the opening tag of the table.

Then we go into a double loop where we append <TR> (table row) elements that contain a set of <TD> (table data) elements. Note that each <TD> represents a single cell, and it contains an <IMG> tag that specifies whether the cell should display a life or not. Note also that each of the <IMG> tags has a unique ID, which contains the row and column number. We'll use this ID to identify which cell was clicked in our Microsoft® JScript code later on. (Using these IDs saves us from having to try to calculate the location from pixel offsets. Instead, we let the browser report the ID of the item clicked, then parse the subscripts from the ID.)

Finally, we close each tag, and then return the string builder as a string.

Rendering the life board array to a string so we can store it in a hidden input element is even simpler. It's similar to the above code, but only renders "0" and "1" characters to indicate whether a life is present or not:

    Private Function RenderLifeToString(ByVal board As Integer(,)) _
    As String
        Dim ret As New StringBuilder(rows * cols)
        ret.Length = rows * cols
        Dim i, j As Integer
        Dim charCount As Integer = 0
        For i = 0 To rows - 1
            For j = 0 To cols - 1
                ret(charCount) = _
                    IIf(board(i, j) = 0, "0", "1")
                charCount += 1
            Next
        Next
        Return ret.ToString()
    End Function

In this case, we allocate the StringBuilder to the proper length by calling the proper constructor to set its maximum length, then setting its length properly, causing the StringBuilder to be grown to the proper size. Then we set each character properly. Doing this is faster than using Append or AppendFormat, but in the table rendering, it would be hard to build the string without using the append methods, since the sizing of the elements can be variable.

ASP.NET then renders the entire page, including our DIV and hidden INPUT, and sends it to the browser.

Second: What happens at the browser

The page is then loaded by the browser and displayed after running the OnLoad function. We'll discuss this function in the section on the timer (below). On the first time through, the timer is not set, so the OnLoad function returns without doing anything.

The user is then free to interact with the page. If the user clicks any button other than the timer toggle button, a request is generated for the server, and the cycle begins again with the page load event being fired on the server, the event being handled, and the page being rendered with the new values.

If the user clicks the timer toggle button, JScript is executed to set up the timer. This takes three functions to work right, so we'll discuss it separately.

The other thing the user might do is to click on the life board table itself. The click handler for editing the life board follows:

function LifeBoard_onclick() { 
imgID = event.srcElement.id; 
sa = imgID.split("_"); // format is "IMG__" 
// sa[0] should contain "IMG" 
if (sa[0] == "IMG") { 
row = sa[1]; col = sa[2]; 
// 32 is the number of columns--be sure to change if 
// you change it on the server side!!!! 
linearPos = row * 32 + new Number(col); 
with (document.Form1) { 
firstPart = LifeData.value.substr(0, linearPos); 
lastPart = LifeData.value.substr(linearPos + 1); 
if (LifeData.value.charAt(linearPos) == "0") { 
middlePart = "1"; 
document.getElementById(imgID).src = "alive.gif"; } 
else { 
middlePart = "0"; 
document.getElementById(imgID).src = "empty.gif"; 
} 
LifeData.value = firstPart + middlePart + lastPart; 
} 
} 
}

This client-side code first finds the ID of the image that was clicked, then splits that ID into an array of sub-strings. It then ensures that the ID begins with "IMG" and proceeds to change the URL of the appropriate IMG tag so it displays properly. We also change the contents of the hidden input element at the proper spot so that we can pass the data back to the server side correctly. Note that this function does NOT generate any sort of server-side processing—all the processing here is on the client!

JScript doesn't have a way to change an individual character in the middle of a string, so we need to create a new string by concatenating the old string until just before the changed character, the changed character, and the remainder of the old string. In other words, if we want to change the middle character ("X") in "000X111" to a "Y", we'd concatenate the unchanged first three characters, "000", the changed character, "Y", and the unchanged final three characters, "111".

How the timer works

The timer is entirely done on the client side (with the exception, of course, of making a request to the server to calculate a new board).

The fact that we DO make a request to the server means that all of our JScript variables will be cleared when the page comes back—so if we're going to keep the state of the timer, we need to do it in a way that will be durable from request to request.

For this, we choose a hidden input element. Here's the code that toggles the hidden input element (and therefore the state of the timer):

function ToggleAuto_onclick() {
   with (document.Form1) {
      if (TimerEnabled.value != "true") { // timer not running
         // hidden form value for postback
         TimerEnabled.value = "true";
         // change button right away in case server slow
         ToggleAuto.value = "Stop!!!";
         ToggleAuto.style.backgroundColor = "red";
         // run 1st generation, rest handled by onload
         RunSingle.click();
      }
      else {
         // don't change if already refreshing
         if (!TickHappening) {
            // shut off timer, clear reference 
            clearTimeout(TimerID);
            // hidden form value for postback
            TimerEnabled.value = "false";
            // change button in UI
            ToggleAuto.value = "Start repeating";
            ToggleAuto.style.backgroundColor = "Lime";
         }
      }
   }
}

Here, if the timer isn't running (the initial state), we change the hidden input element to contain "true", and then we change the button to a stop state and run the first new generation. When we come back, the OnLoad function (below) will set the timer so that the generation after that will be calculated. Here's OnLoad:

function window_onload() {
   with (document.Form1) {
      // page loading, timer off; should we set?
      if (TimerEnabled.value == "true") {
         msec = RepSecs.value * 1000;
         if (msec < 2000) { // not too fast!
            msec = 2000;
            RepSecs.value = "2";
         }
         // have to override default page values
         ToggleAuto.value = "Stop!!!";
         ToggleAuto.style.backgroundColor = "red";
         // set the timer...note only one interval
         TimerID = setTimeout(TickHandler, msec);
      }
   }
}

If the timer is on (according to the hidden input element), we calculate the number of milliseconds, based on the value of an INPUT element to set the timer (to 2000 msec, or two seconds, minimum), change the button color/text (if not changed already), and then set a countdown timer.

When the timer fires, the tick event-handler method is called:

function TickHandler() {
   with (document.Form1) {
      if (TimerEnabled.value == "true") { // eliminate stray ticks
         TickHappening = true;
         RunSingle.click();
      }
   }
}

This function mainly clicks the single-step button, but before it does, it makes sure that the timer is still on. Without this check, it's possible for a tick to come just after you've toggled the timer off. We also set a variable called TickHappening so that it can be checked in the button toggle method—again, preventing race conditions.

All of the other functionality is handled on the server, and most of that code is very similar, but not the same, as the Windows Forms application from last time.

Something Borrowed, and...

So let's look at what happens when we click on one of the server-side event buttons. First, the Page_Load event handler we showed above is called. This time, there is an array to get, so we build an array from the string stored in the hidden input element. The code for parsing that is here:

    Private Function GetBoardFromString(ByVal s As String) As Integer(,)
        If (s.Length <> rows * cols) Then ' string not present
            Return Nothing
        Else
            Dim board = New Integer(rows - 1, cols - 1) {}
            Dim i, j As Integer
            For i = 0 To rows - 1
                Dim rowStart As Integer = i * cols
                For j = 0 To cols - 1
                    board(i, j) = _
                        IIf(s.Chars(rowStart + j) = "0", 0, 1)
                Next
            Next
            Return board
        End If
    End Function

If the string is not present or is of the wrong length, we just return Nothing as an error. Otherwise, we create an array, and then parse the string to set the elements of the array.

Next, if a button was clicked, one of the following event handlers will be called:

    Private Sub AddRandom_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles AddRandom.Click
        FillRandom(NumRnd.Text, False)
    End Sub

    Private Sub Clear_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles Clear.Click
        ' array not created in OnLoad; must create now!
        currBoard = New Integer(rows - 1, cols - 1) {}
    End Sub

    Private Sub RunSingle_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles RunSingle.Click
        currBoard = CalcNext(currBoard)
    End Sub

    Private Sub RunMult_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles RunMult.Click
        Dim num As Integer = NumOfGen.Text
        Dim i As Integer
        For i = 1 To num
            currBoard = CalcNext(currBoard)
        Next
    End Sub

These handlers do pretty much what they did last time, but since the page is rendered automatically, they don't need to call a routine to render and update.

Also, the clear handler works differently: Rather than creating an array in the load handler and then clearing it, we just skip creating the array in the load handler (recall the if statement text) and create the array anew in the clear handler.

Finally, the run-multiple-generations handler is new; it just calls CalcNext in a loop.

Something Blue!

Well, there isn't really much blue here, but for the sake of having a good wedding between ASP.NET and Life, we could consider the fact that there's no HTML-based slider control to be a sad thing, and that could make us blue.

Design Choices

Any application involves some choices between various designs. The most obvious one in this application was deciding what stuff should be done on the server and what stuff should be done on the client.

It's clear that you could do all the processing on the client, including calculating generations. JScript is pretty slow, so the good doctor is speculating that doing the life calculation in JScript might be slower than doing it on the server. (But Dr. GUI admits he hasn't tested this, and on a really fast desktop, it might be faster to do this client side, even using script.)

On the other hand, everything could be on the server side, including editing the board. But editing the board server side would be painfully slow, since a round trip to the server would be required for each editing change. Ouch!

You could really argue about whether the clear event should be done on the server or client. It would be easy enough to do this on the client, but we already had the server-side code running, and users don't expect clear to be very fast, so the good doctor just left good enough alone. (If it ain't broke, don't fix it!)

Give It a Shot!

If you've got .NET, the way to learn it is to try it out ... and if not, please consider getting it. If you spend an hour or so a week with Dr. GUI .NET, you'll be an expert in the .NET Framework in no time at all!

Be the First on Your Block—and Invite Some Friends!

It's always good to be the first to learn a new technology, but even more fun to do it with some friends! For more fun, organize a group of friends to learn .NET together!

Some Things to Try...

  • First, try out the code shown here. Play with the code some.
  • Add some different buttons to do fun things. Maybe have a menu of stock patterns you can add to the board.
  • Try this with different data structures and see how it works out.

What We've Done; What's Next

This time, we did Conway's Game of Life as an ASP.NET application—with a few strategic client-side JScript methods. Next time, we'll show how formatting and parsing works in the .NET Framework.