January 2009 - Posts

Using modules for classic ASP with II7 Integrated Mode
Thursday, January 22, 2009 7:39 PM

Using ASP.NET HttpModules it is very easy to perform additional operations before and after requests. Since this post is not focused entirely on modules I’ll direct you to this very neat link about modules.

One of the things that we can do to show how modules work is to add headers or footers to the Response. However for those of us who have ever attempted to do this with classic ASP, the story is a little less straight forward. If the module adds a custom header and footer to the response, on viewing the page you might find that the response is truncated or incomplete. Let’s see a little bit of this in action.

Make sure you’re using IIS in integrated mode. Create a new ASP.NET website and create a new class. Let’s call it HelloWorldModule.cs.

using System;
using System.Web;
using System.Collections;
using System.Collections.Specialized;


/// <summary>
/// Summary description for HelloWorldModule
/// </summary>
public class HelloWorldModule : IHttpModule
{
    public String ModuleName
    {
        get { return "HelloWorldModule"; }
    }

    // In the Init function, register for HttpApplication 
    // events by adding your handlers.
    public void Init(HttpApplication application)
    {
        application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
        application.EndRequest += (new EventHandler(this.Application_EndRequest));

    }

    // Your BeginRequest event handler.
    private void Application_BeginRequest(Object source, EventArgs e)
    {
        HttpApplication application = (HttpApplication)source;

        HttpContext context = application.Context;

        context.Response.Write(
            "<h1><font color=red>HelloWorldModule: Beginning of Request</font></h1><hr>");

    }

    // Your EndRequest event handler.
    private void Application_EndRequest(Object source, EventArgs e)
    {
        HttpApplication application = (HttpApplication)source;
        HttpContext context = application.Context;
        context.Response.Write(
            "<hr><h1><font color=red>HelloWorldModule: End of Request</font></h1>");


        try
        {
            int cookiesCount = context.Response.Headers["Set-Cookie"].Split(';').Length;
            int count = context.Response.Cookies.Count;
            context.Response.Write("Number of Cookies : " + count + "</hr>");
        }
        catch (Exception ex)
        {
            context.Response.Write("Failed" + "</hr>");
        }

    }

    public void Dispose()
    {
    }

    public HelloWorldModule()
    {
        //
        // TODO: Add constructor logic here
        //
    }
}

You can see that the Application_BeginRequest and Application_EndRequest methods add a header and footer to the Response. The Application_EndRequest method also displays the number of cookies added.

Next we would need to register the module with our web.config. We can do that by adding the following between the <httpModules> and <httpModules/> nodes.

<add name="HelloWorldModule" type="HelloWorldModule"/>

Now let’s create two very simple pages. One using classic ASP and the other using ASP.NET and see the difference in how the response is displayed. Both pages in essence do the same thing i.e. set the values of two cookies.

Here’s how our ASP.NET page would look:

<%@ Page Language="C#"%>

<!DOCTYPE html PUBLIC 
"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <% 
        Response.Cookies.Set(new HttpCookie("foo", "bar"));
        Response.Cookies.Set(new HttpCookie("foo2", "bar2"));
     %>    
    </div>
    </form>
</body>
</html>

And now our classic ASP page:

<%
Response.Cookies("foo") = "bar"
Response.Cookies("foo2") = "bar2"
 %>

 

Let’s go ahead and browse both these pages now. The ASP.NET page first … which looks exactly how we expected. And then the ASP page which doesn’t quite look like we wanted it to.

The reason? ASP and a lot of other ISAPI extensions are likely to believe that they are the sole content generator for the response. And ASP.NET being a newer programming interface, while modifying the response, needs to be aware of this. Classic ASP sets the “Content-length” equal to the length of the content on the asp page and of course only that number of characters from the response are displayed when the page is browse. The additions made to the response increase the length of content but never inform ASP, since there really isn’t any way ASP.NET could talk to ASP directly. Luckily, there are adjustments so make ASP behave like we would want it to. In this case, that would be adjusting the value of “Content-length” or removing it.

So let’s remove the Content-Length header from the response in the Application_EndRequest method of our module. Now it should look like:

private void Application_EndRequest(Object source, EventArgs e)
    {
        HttpApplication application = (HttpApplication)source;
        HttpContext context = application.Context;
        context.Response.Write(
"<hr><h1><font color=red>HelloWorldModule: End of Request</font></h1>");
        

        try
        {
            int cookiesCount = context.Response.Headers["Set-Cookie"].Split(';').Length;
            int count = context.Response.Cookies.Count;
            context.Response.Write("Number of Cookies : " + count + "</hr>");
        }
        catch (Exception ex)
        {
            context.Response.Write("Failed" + "</hr>");
        }
        context.Response.Headers.Remove("Content-Length");

    }

And browse … Looks better, doesn’t it? Depending upon your application and its use there might be other adjustments that you might need to make. The point to take home is when using classic ASP with newer programming interfaces one needs to be careful of what ASP is aware of and what it isn’t so that adjustments can be made to get the desired results.

Let me know if you have any questions!

Assad Safiullah

Software Development Engineer in Test

ASP.NET QA Team

ASP.NET 4.0 ClientID Overview
Tuesday, January 06, 2009 4:57 PM

Introduction

One of the new features being added to version 4.0 of ASP.NET is the ability to control the client side IDs that are generated by the framework.  Previously the framework would modify the client side IDs to uniquely identify each control.  This some times left you with the ID you defined in markup or sometimes left you with something that looks like this, “ctl00_MasterPageBody_ctl01_Textbox1.”

The Problem

The modification of the client side id property works great to ensure that each element is uniquely identified, however, to anyone that has tried to do any sort of client side scripting this becomes very frustrating. Chances are that if you have worked in ASP.NET for any time at all you have run into this issue.  The problem is that until runtime you do not what the client side ID could be, making it difficult to do any kind of client side scripting.  In addition any modification of the page, adding removing controls, can result in a different client side ID being generated.

Old Solution

Again if you have worked with ASP.NET for any amount of time you know there is a work around for this issue.  Each control has a property called ClientID that is a read only and supplies the unique client side ID.  You can use this in a code behind when dynamically adding scripts, or more commonly use inline code (old ASP style) to supply the value to and client side scripts.

<script type="text/javascript">
    function DoSomething(){
        alert('<%= Control.ClientID %>');
    }
</script>

ASP.NET 4.0 Solution

First off let me start by explaining why we decided to tackle this problem in version 4.0 of the framework.  While we provided a way of supplying the developer with the client side ID, with the growth of client side scripting this solution has become some what hacky.  There is not really a clean way to use this with lots of controls and lots of external script files.  Also it might have had something to do with the developer asking for control over this.  Developers do love to have control of everything, weather they use it or not, it’s just our nature :) The solution that we came up has four ‘modes’ that a user can use giving them everything from existing behavior to full control.  The controls ID property is modified according to the ClientIDMode mode and then used as the client side id.

Modes and what they do

There is now a new property on every control (this includes pages and master pages as they inherit from control) called ClientIDMode that is used to select the behavior of the client side ID.

<asp:Label ID="Label1" runat="server" ClientIDMode="[Mode Type]" />

The Mode Types

  • Legacy: The default value if ClientIDMode is not set anywhere in the control hierarchy.  This causes client side IDs to behave the way they did in version 2.0 (3.0 and 3.5 did not change this code path) of the framework. This mode will generate an ID similar to “ctl00_MasterPageBody_ctl01_Textbox1.”
  • Inherit: This is the default behavior for every control.  This looks to the controls parent to get its value for ClientIDMode.  You do not need to set this on every control as it is the default, this is used only when the ClientIDMode has been changed and the new desired behavior is to inherit from the controls parent.
  • Static: This mode does exactly what you think it would, it makes the client side ID static. Meaning that what you put for the ID is what will be used for the client side ID.  Warning, this means that if a static ClientIDMode is used in a repeating control the developer is responsible for ensuring client side ID uniqueness.
  • Predictable: This mode is used when the framework needs to ensure uniqueness but it needs to be done so in a predictable way.  The most common use for this mode is on databound controls.  The framework will traverse the control hierarchy prefixing the supplied ID with it’s parent control ID until it reaches a control in the hierarchy whose ClientIDMode is defined as static.  In the event that the control is placed inside a databound control a suffix with a value that identifies that instance will also be added to the supplied ID.  The ClientIDRowSuffix property is used to control the value that will be used as a suffix (see samples).  This mode will generate an ID similar to “Gridview1_Label1_0”

Samples

Legacy Mode

Legacy mode is pretty straight forward, it generates a client side ID the way that it had in version 2.0 of the framework.

markup:

<asp :TextBox ID ="txtEcho" runat ="server" Width ="65%" ClientIDMode ="Legacy" /> 

output:

<input id="ctl00_MasterPageBody_ctl00_txtEcho" style="width: 65%" 
name="ctl00$MasterPageBody$ctl00$txtEcho" />

Static Mode

Static is the most basic of all ClientIDMode modes, what you give for the ID is what you get for the client side ID. Once again a warning that if a static ClientIDMode is used inside of a repeated control it is the developer’s responsibility to ensure client side ID uniqueness.

markup:

<asp:TextBox ID="txtEcho2" runat="server" Width="65%" ClientIDMode="Static" />

output:

<input id="txtEcho2" style="width: 65%" name="ctl00$MasterPageBody$ctl00$txtEcho2" />

Predictable Mode

Predictable mode really tackles the heart of the problem.  The framework previously generated it’s unique IDs to prevent ID collisions and the most common place for these types of collisions are inside databound controls.  Predictable mode is really designed to work with databound controls but does not have to.  There is three ways to uses the predictable mode, each one of these is defined through the ClientIDRowSuffix property that specifies the suffix for each instance.  The ClientIDRowSuffix uses values from the control’s datakeys collection, so if the control does not have a datakeys collection this property is not viable.  If this property is not set or is not available the row index will be used in it’s place.

1. With no ClientIDRowSuffix defined, this is also the behavior for databound controls without a datakeys collection e.g. Repeater Control.  Notice that the framework has traversed the control hierarchy and prefixed the ID with the parent’s ID and suffixed the ID with row index.

markup:

<asp:GridView ID="EmployeesNoSuffix" runat="server" AutoGenerateColumns="false" 
ClientIDMode="Predictable" > <Columns> <asp:TemplateField HeaderText="ID"> <ItemTemplate> <asp:Label ID="EmployeeID" runat="server" Text='<%# Eval("ID") %>' /> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Name"> <ItemTemplate> <asp:Label ID="EmployeeName" runat="server" Text='<%# Eval("Name") %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView>

output:

<table id="EmployeesNoSuffix" style="border-collapse: collapse" cellspacing="0" rules="all" border="1">
    <tbody>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">Name</th>
        </tr>
        <tr>
            <td><span id="EmployeesNoSuffix_EmployeeID_0">1</span></td>
            <td><span id="EmployeesNoSuffix_EmployeeName_0">EmployeeName1</span></td>
        </tr>
        ...
        <tr>
            <td><span id="EmployeesNoSuffix_EmployeeID_8">9</span></td>
            <td><span id="EmployeesNoSuffix_EmployeeName_8">EmployeeName9</span></td>
        </tr>
    </tbody>
</table>

2. With a ClientIDRowSuffix defined, this looks in the control’s datakeys collection for the value and then suffixes the ID with that value.

markup:

<asp:GridView ID="EmployeesSuffix" runat="server" AutoGenerateColumns="false" 
ClientIDMode="Predictable" ClientIDRowSuffix="ID" > <Columns> <asp:TemplateField HeaderText="ID"> <ItemTemplate> <asp:Label ID="EmployeeID" runat="server" Text='<%# Eval("ID") %>' /> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Name"> <ItemTemplate> <asp:Label ID="EmployeeName" runat="server" Text='<%# Eval("Name") %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView>

output:

<table id="EmployeesSuffix" style="border-collapse: collapse" cellspacing="0" rules="all" border="1">
    <tbody>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">Name</th>
        </tr>
        <tr>
            <td><span id="EmployeesSuffix_EmployeeID_1">1</span></td>
            <td><span id="EmployeesSuffix_EmployeeName_1">EmployeeName1</span></td>
        </tr>
        ...
        <tr>
            <td><span id="EmployeesSuffix_EmployeeID_9">9</span></td>
            <td><span id="EmployeesSuffix_EmployeeName_9">EmployeeName9</span></td>
        </tr>
    </tbody>
</table>

3. With a ClientIDRowSuffix defined, but instead of just one value a compound value will be used.  Exhibits the same behavior as one value but it will suffix both values onto the ID.

markup:

<asp:GridView ID="EmployeesCompSuffix" runat="server" AutoGenerateColumns="false" 
ClientIDMode="Predictable" ClientIDRowSuffix="ID, Name" > <Columns> <asp:TemplateField HeaderText="ID"> <ItemTemplate> <asp:Label ID="EmployeeID" runat="server" Text='<%# Eval("ID") %>' /> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Name"> <ItemTemplate> <asp:Label ID="EmployeeName" runat="server" Text='<%# Eval("Name") %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView>

output:

<table id="EmployeesCompSuffix" style="border-collapse: collapse" cellspacing="0" rules="all" border="1">
    <tbody>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">Name</th>
        </tr>
        <tr>
            <td><span id="EmployeesCompSuffix_EmployeeID_1_EmployeeName1">1</span></td>
            <td><span id="EmployeesCompSuffix_EmployeeName_1_EmployeeName1">EmployeeName1</span></td>
        </tr>
        ...
        <tr>
            <td><span id="EmployeesCompSuffix_EmployeeID_9_EmployeeName9">9</span></td>
            <td><span id="EmployeesCompSuffix_EmployeeName_9_EmployeeName9">EmployeeName9</span></td>
        </tr>
    </tbody>
</table>

Summary

The ability to fully control the client side IDs that are generated by the framework is a request that has not generated much noise but everyone seems to want it when you mention it.  We believe that we have found a good solution to the request and think that it adds some much need functionality for developer that use lots of client side scripting.  There is an early preview and a walk through of this feature in CTP build that we released at PDC 2008.  For more information and a much more detailed description of this feature read Scott Galloway’s blog post.

by osbornm | 482 comment(s)
Filed under:
AJAX History and ASP.NET AJAX Preview 3
Tuesday, January 06, 2009 12:54 PM

I hope everybody had a great holiday break and is ready to go for 2009 :).

In .NET 3.5 SP1 we shipped some improvements to the AJAX framework, including the AJAX History Feature, which allows you to add history points in the browser for AJAX applications.  There have been some great blog posts about how to use the history feature; Jonathan Carter has a post specifically talking about it client-side here

Jonathan's sample is clean and simple, and showcases the feature well. He includes a lot of great detail on the intricacies and inner workings of AJAX client-side history.  I'm actually going to refer to his post a lot as I build a similar demo, making use of some of the cool new features in ASP.NET AJAX Preview 3.  If you are unfamiliar with the history feature, now would be a good time to read through the first two parts of Jonathan's series on ASP.NET AJAX History here and here, as I'm going to assume some basic knowledge about the feature.

To start off my sample site, I've done a lot of the same setup steps as my last app: basically I've created a new web application and added the ASP.NET AJAX Preview 3 scripts into the project.  If you're unsure how to get up until that point, please refer to this post and follow the instructions under "Website Setup".

One difference is that my ScriptManager doesn't need the MicrosoftAjaxAdoNet.js file since I won't be using data services in this app. and also that my ScriptManager now needs to enable the AJAX History scripts by setting EnableHistory="true". 

<asp:ScriptManager ID="ScriptManager1" EnableHistory="true" runat="server">
<Scripts>
    <asp:ScriptReference Path="~/Scripts/MicrosoftAjaxTemplates.js" />
</Scripts>
</asp:ScriptManager>

If you've read through Jonathan's post, you'll note that he's done some work to get his selected item concept working.  With ASP.NET AJAX Preview 3, we get this functionality for free through the DataView, so let's go ahead and set that up now.

I'm using a default StyleSheet, Default.css, which is available in the zip file of the project here.  I've also added some classes for the body and selectedItem which we will use later.  So my style section looks like this:

<style type="text/css">
    @import url(Default.css);
    .sys-template { display:none }
    body 
    {
        width: 350px;
        margin: 0px auto;
    }
    .selectedItem 
    {
        text-decoration:none;
        color:White;
        background-color:Black;
    }
</style>

I'm also going to go ahead and set up some JavaScript data at this point so I can have an array to bind to when we set up the dataview.  Here I've created a list of people, using members of the AJAX team.

var myList = [{ name: "Jim Wang", location: "Vancouver" }, 
              { name: "Dave Reed", location: "Los Angeles" },
              { name: "Clay Compton", location: "Kirkland" },
              { name: "Nghi Nguyen", location: "Redmond" },
              { name: "Bertrand Le Roy", location: "Seattle" },
              { name: "Boris Moore", location: "Bellevue" }];

So now I can set up my master and detail view in declarative markup, where I show the name of the person in the master, and display the location in the detail.  Here's the master view:

<fieldset>
<legend>Person</legend>
<div id="personListMaster" sys:attach="dv" 
        dv:data="{{myList}}" 
        dv:initialselectedindex="-1" 
        dv:selecteditemclass="selectedItem" 
        dv:sys-key="master" >
    <a sys:command="select">{{name}}</a><br />
</div>
</fieldset>

I've disabled the default selected index of 0 (and set it to -1 declaratively) so that when the page renders, there will be no item selected initially.  I've also added the selectedItem class declaratively, and set a sys-key on the master, so I can refer to it in the detail view without doing a $find.  Here's the detail view:

<fieldset>
<legend>Location</legend>
<div id="personListDetail" sys:attach="dv" 
        dv:data="{binding selectedData, source={{master}}}">
    <span>{{location}}</span>
</div>
</fieldset>

As you can see, I'm able to bind to the selectedData property of the master simply by referring to it by its sys-key, which is cleaner than doing a $find.

So at this point the site should be operational: you can load it up and click on a name to populate the detail view like so:

image

Now that we have the selection of names working, let's add the history feature.  The first thing I'm going to do is add a command handler to my master view.  If you look at the master view, you'll see that the link has the sys:command="select" property associated with it.  This command is special, it means that when you click on that item, it will become selected and the selected item class, if available, will be applied.  However, we can also add to this functionality by adding a handler for the event, and passing in a command argument, which we will do for the purposes of this demo.  So my new master view looks like this:

<fieldset>
<legend>Person</legend>
<div id="personListMaster" sys:attach="dv" 
        dv:data="{{myList}}" 
        dv:initialselectedindex="-1" 
        dv:selecteditemclass="selectedItem" 
        dv:sys-key="master" 
        dv:oncommand="{{personClicked}}">
    <a sys:command="select" sys:commandargument="{{$index}}">{{name}}</a><br/>
</div>
</fieldset>

So I've added a handler called personClicked (which I'm going to have to define in my JavaScript), and I'm passing a commandargument of $index.  $index is a special pseudo-column, accessible within a template/dataview, that gives the current index of the item being rendered, starting from 0.  So in my case, when this dataview is rendered, since I have 6 names,  I will generate $index values of 0 to 5.

With this new code, when I click on a name, it is selected, bubbles the command up to the dataview, and then the personClicked handler is called and passed an argument of $index, which will vary depending on which name was clicked (which is exactly what we want, since we need to be able to tell which person was clicked).

Here's the code for personClicked, which is where we will add our history point.

function personClicked(sender, args) {
    if (args.get_commandName() === "select") {
        var mySelectedIndex = args.get_commandArgument();
        var myTitle = myList[mySelectedIndex].name;
        Sys.Application.addHistoryPoint({selected : mySelectedIndex}, myTitle);
    }
}

So here, I'm ensuring that the command is a "select" before doing further processing.  In general, you can make up your own command names, so I could specify sys:command="myFoo" to an element, and then, if I specified a command handler, I would be able to pick out that command using the get_commandName() API.  However, in this case, I'm only interested in "select".  After I've verified that the event was a select, I pull out the command argument, which tells me the index of the person who was clicked on.  Since I'm dealing with a fixed array, I can immediately get the name of the person associated with that index by looking at the array.  Finally, I can add my history point with these parameters.  I've arbitrarily named the property "selected", because all I need to save in state is the currently selected index.  Also note that the user can see this property in the URL, so making it compact and meaningful is important.  I'm also passing in the optional title property to change the title of the website (and history entry) to the name of the person who has been clicked.

This might be a good time to note that although passing the $index pseudo-column in as a command argument is one way to get the selected index in the personClicked handler, you can also access the same information directly from the sender and args like so:

mySelectedIndex = sender.getItem(args.get_commandSource()).index;

In this case, the sender is the DataView, so we call the getItem API on the source of the command (which is the element being clicked), and look at the index property.  So using this method, although slightly more complex, eliminates the need to pass a command argument to the handler, but I'm going to leave the command argument implementation as is for illustration purposes.

If I didn't want to use a command handler, I could also hook into the selectedIndex change event, enabling me to pick up changes in the selected index from things other than the user clicking on an item (for example, if I was to change the selected index in code using the DataView's set_selectedIndex() API).   For simplicity and illustration purposes, I'm going to leave the command handler implementation as is, but if you wanted to hook into the selectedIndex change event, you could use the following code to add a property changed handler to the dataview, and then add the history point:

function pageLoad() {
    $find("personListMaster").add_propertyChanged(myHandler);
}     
function myHandler(sender, args) {
    if (args.get_propertyName() == "selectedIndex") {
        var mySelectedIndex = $find("personListMaster").get_selectedIndex();
        var myTitle = myList[mySelectedIndex].name;
        Sys.Application.addHistoryPoint({selected: mySelectedIndex}, myTitle);
    }
}

Now, all that is left to do is add a handler for the Navigate event so that the page knows what to do with the history points.  We can do this in init like so (put this snippet right after the ScriptManager, otherwise you won't be able to resolve the Sys namespace):

<script type="text/javascript">
    Sys.Application.add_init(function() {
        Sys.Application.add_navigate(navigatePerson);
        });
</script>

With the associated navigatePerson function defined as follows:

function navigatePerson(sender, args) {
    var clickedSelectedIndex = args.get_state().selected;
    $find("personListMaster").set_selectedIndex(clickedSelectedIndex);
    document.title = clickedSelectedIndex ? 
                myList[clickedSelectedIndex].name : "Ajax History Blog Post";
}

Note that here I'm pulling only the index out of the state information, and then using that to directly set the selectedIndex of the dataview.  I'm also restoring the old title information by setting document.title to the name of the person if the clickedSelectedIndex is defined, and setting it back to the default title if it isn't defined.  So now if we load up the application, we should have full history support, being able to click on different people, and then using the browser's back button to jump back to previous states.  One cool thing is that the detail view data is also seamlessly saved (which makes sense since we only depend on the selected index, and the detail view is live-bound to the selected data of the master).  A typical URI looks like this:

http://localhost:18142/AjaxHistory/Default.aspx#selected=5  

Which is as expected.  Note that from a security standpoint, I can create my own user states by simply changing the last number in the URI, which is why you shouldn't store any critical information in unencrypted client-side AJAX history.  In our case, this isn't a risk because all we're doing is changing the selected item.

A final note is that  in this simple example, I'm not changing the array of items at all, so my selected index is always going to be consistent.  However, if I were to add the ability to add and remove items from the array (perhaps using Sys.Observer), this example is going to break, because when I modify the collection, I change the selected index values.  So for example, if I had Dave selected, but I removed Jim, Dave's selected index goes from being 1 to 0, which means that I cannot accurately reconstruct the state of the page using selected index alone, and will need a more complex data key (preferably a primary key) to reconstruct the user's state.  However, for this simple example, using the selected index is lightweight and works fine.

I hope this post has given you some good ideas on how to use the new ASP.NET AJAX stuff to do creative things with older legacy features.  The big things that stick out for me are that we didn't have to loop through our elements and add handlers because of the command bubbling architecture of dataview, and that we didn't have to write any code to get selected items working.  The full website is attached as a zip to this post.  Happy Coding!

More Posts