So far, we’ve discussed:
- Creating the basic data model for the WebGallery.
- Changes to the HttpHandler to increase speed and efficiency.
- Building and viewing the individual galleries–created using clean, simple HTML code.
Our galleries are now visible; however, we need a few administration tools to handle changes, such as updating the title and description or fixing a broken thumbnail (since they’re pre-generated now).
NOTE: Code downloads are available on the Introduction post.
This post will cover the topic of using the ModalPopupExtender to manage files once their added into the galleries.
In our last two posts, we’ve discussed the underlying data model and how LINQ and the HttpHandler wraps everything up to display our photos.
NOTE: Code downloads are available on the Introduction post.
This post will cover how the galleries use those data components and controls such as the Data Pager and ListView to build fully XHTML compliant code.
In an upcoming post for the WebGallery2 project, I needed a method to return a group of galleries and files based on last visit date (as compared to the creation date of the gallery item). In TSQL, that’s easy enough like:
SELECT w.Name, w.GalleryId, g.Name from WebFiles w, Galleries g
WHERE w.DateCreated >= ’01/01/2008′
AND w.GalleryId = g.Id
GROUP BY g.Name, w.GalleryId, w.Name
From there, you can iterate through the results, and build your controls.
Now, how would you do that in LINQ? The GroupBy extension method.
The GroupBy extension method returns a result of IGrouping<K, V>, which you can then loop through in your application, create lists, etc.
For example, using the same logic as the TSQL above:
public List<IGrouping<Gallery, WebFile>> GetAllSinceLastVisit(
DateTime lastVisitDate, IPrincipal user)
var availableGalleries = GetGalleriesByRole(user);
.Where(i => i.DateCreated >= lastVisitDate &&
.GroupBy(i => i.Gallery)
This returns a List of IGrouping<K,V> with Galleries as the key and the WebFiles as values. To demonstrate how to build a quick list, we can write out an unordered list of “new” files since 1/1/2008. (the GetGalleriesByRoles ensures that its only parsing the galleries that the user has access to).
foreach (var result in db.GetAllSinceLastVisit(new DateTime(2008, 1, 1)))
Response.Write(result.Key.Name + “<br/>”);
foreach (var file in result)
Response.Write(“<li>” + file.Name + “</li>”);
With anonymous types, the var’s automagically detect what type of underlying object (Type) the key and values are and intellisense automatically picks up the properties and methods.
Since we have full access to the properties of each web file (and gallery), we could easily make these into hyperlinks or thumbnails by modifying the values loop (foreach var file in results):
This is the second in a series of posts describing the changes to the WebGallery in version 2.0. This post will cover the changes and coding techniques applied to the HttpHandler and the new LINQ-to-SQL data model.
NOTE: Code downloads are located on the introduction post (easiest way to keep it current).
Over a year ago, I started creating the original “Photo Gallery,” which became the blueprint for my online photo and file repository. I created the site because commercial solutions, like Flickr and SkyDrive, couldn’t provide me with exact URLs of both thumbnailed and full-size objects. Google’s Picasa got close, but still required using their interface—which REALLY annoyed me if I wanted to post up photos for the blog.
With a year of experience behind me, and seeing what I liked/disliked about how WebStorage (its eventual name) worked, I set out to create version 2.0.
Download Code: UPDATED 3 march 2008: build 38
Changes in this version:
- Built on .NET 3.5.
- Fully XHTML 1.1 compliant.
- Compatible (mostly) and tested with with FireFox (2, 3pr), IE (6, 7), Opera (9.5), and Safari (3.0.4).
- Removed Enterprise Library dependencies in favor of LINQ-to-SQL.
- Resolution of file handing “issues” in v1.0.
- Implementation of ASP.NET AJAX to speed up gallery rendering.
- Added “paging” to the galleries.
- Removed ASP.NET user controls in favor of cleaner ListViews.
- Added caching to the WebStorageHandler.
- Added Deflate and GZip HTTP Compression to the .aspx and .ashx pages.
- Reengineered thumbnail managed (pre-fab’ed now) to speed up load times. (Thanks for the idea, Michael!)
- and more!
Over the next few blog posts, I’m going to touch on each of the changes (noted above) and the reasoning behind them; however, I wanted to get the code out there.
The project’s laid out in a simply hierarchy—a folder per respective “area,” including the LINQ-to-SQL models. I build the project in such a way to, I hope, easily move into the MVC frameworkThe additional App_Browsers directory is used by our CSS Friendly adapters.
This project is based on ASP.NET 3.5 (without the 3.6 extensions) and a couple of additional libraries:
- Microsoft CSS Friendly Adapters, build 9278.
- Blowery HTTPCompressionModule (alternative to built-in .NET), revision 4.
I also used Visual Studio 2008’s new web tests (seen in the screenshot) to complete load testing and tidy up code. I haven’t included that in the source code, but I plan to do a post on the features and results—it’s a very slick system.
With full LINQ-to-SQL support, I opted for LINQ as a solution rather than the Enterprise Library. LINQ encapsulates the SQL logic to keep our code secure, the data context object is a partial class—allowing us to custom code methods and properties into our models, and it just “works.” If you haven’t noticed from other posts of mine, I really like LINQ as an ORM solution—especially now that it’s built into the environment and fully supported by the framework.
The model itself is quite simple. Three tables control a majority of the functionality for the application.
The WebFile table
The WebFile table contains the actual objects for our WebStorage gallery. ObjectBody and ObjectThumbnail are image columns containing the binary information stored for our objects. The rest of the columns are attributes providing either end-user information (such as Description) or rendering information (such as ObjectType).
A change from the previous data model is that thumbnails are stored along with the web files and are not “automagically” generated—saving both computation time on the server and speeding up rendering for the client.
For our two image fields, I’ve also enabled “lazy loading” or “delay loading”—an attribute that only returns these fields if they are explicitly used within the code and only once per query to save bandwidth.
The ContentType table
The ContentType table manages the MIME types, their thumbnails, and matching object types. This table is used to render icons for PDFs, RARs, ZIPs, and other file types as well as generic icons for photos that do not have thumbnails.
The Gallery table
The Gallery table contains the keys, names, and allowed roles for each gallery. The roles match up to membership roles provided by ASP.NET’s Configuration.
In the next post, I’ll discuss what went into the LINQ-to-SQL queries and how the use of the DataContext’s partial classes sped up development. I’ll also discuss how the ImageHandler has been updated to match these data changes.
Edit: Updated to fix the fonts… I should know better than to create it in Word and then just copy/paste.
The guys at JetBrains came through midafternoon today (evening over there) and have published up the first nightly build of ReSharper 4.0 EAP. These nightly builds, they warn, are VERY early releases, but get the bigs in developers hands.
They did add that anonymous error logs WILL NOT even be looked at, so be sure to sign up for an account to submit responses and allow them to contact for more information—it’s worth it to have a solid release!
In my daily check of the jetbrains.resharper.eap newsgroup, my hopes of a January release of 4.0 EAP continues to dwindle—I mean, there’s only a couple more days left in the month. I hit up the EAP section on JetBrains’ website and found that a 4.0 Roadmap was posted back on 8 January 08, but it’s lacking something kind of important.
Support for both Visual Studio 2005 and 2008
Don’t we have this now?
Comprehensive support for C# 3.0 language features, including:
- New code analysis, context actions and quick-fixes
- New refactorings (such as Convert to Extension Method, Convert to Automatic Property, Convert Anonymous Type to Named)
- Updated IntelliSense
Again, I thought 3.0 support (XAML, etc) was supported in 3.1. Automatic Property support will be really nice. Convert to Extension Method should be interesting. Does this mean full support for WCF, WF, and WPF?
Many more intelligence and productivity features
- Full set of refactorings available for VB.NET
- New quickfixes and context actions for XAML files
- Referenced assemblies are now taken into account in null-reference analysis thanks to external annotations. ReSharper is packaged with annotations for the .NET Framework assemblies
- Code Cleanup — a tool for ensuring compliance with code guidelines and enhancing code structure
- Complete Statement — a feature that will complete language constructs and get you ready to write the next statement
- Many other enhancements and new features throughout the product.
Code Cleanup looks appealing as does Complete Statement, though I’m wondering how processor intensive they’ll be (working up permutations, etc).
So, what’s missing?
There’s no mention of .NET 3.5 support beyond the extension methods and automatic properties.
Nothing about LINQ, Lambda expressions, true 64–bit client support (ReSharper is a 32–bit app right now—I can dream, can’t I?), or AJAX or CSS refactoring. Nothing talking about the newest .NET web development technologies like MVC.
I realize they can’t keep up to the SECOND with everything (though that does speak to a more worthwhile partnership between Microsoft and JetBrains for the end users like myself), but 2008’s been RTM since November and, quite honestly, I haven’t used ReSharper since. I spend too much time with the newer technologies that I found I was wasting time by fighting ReSharper. Does it annoy me? Hell yes. Am I looking for a replacement tool? Yeah, but CodeRush’s “pretty arrows” annoy the crap out of me, so I’m just using macros and built-in refactoring in VS2008.
So… JetBrains—where’s our full 3.5 support and when can we start beating the pavement with the bits?
UPDATE 30 January 2008: From the EAP newsgroup today:
This is the third part of the series looking at rolling out a new Microsoft .NET MVC application. I’ve taken an existing application, the prototype photo gallery I use for this blog (
) and am recreating it (with some extras) inside of the MVC framework—it’s a bit more fun than dinking with Northwind.
With that, let’s take a look at creating a data model and linking data to our controllers and then to our views. This part should fill out our Galleries controller quite nicely.
Adding a Data Model
For these examples, I’m going to use LINQ to SQL. You could just as easily use the new Entity Framework/Astoria, NHibernate, Castle, or any other you wish. I like LINQ.
I’ve pulled down a copy of the existing Photos database, so the examples you see here match what’s in production on the blog’s photo gallery. The first step is to create a LINQ-to-SQL file for the Web Gallery. It’s best, in my opinion, to place these files under the ~/Models directory since, in essence, that’s what they will become.
The two primary tables for the gallery are Galleries and WebFiles. Dragging those two onto the DBML file results in:
I’ll create a quick unit test example—I’ll assume the rest is up and going as these are no different in the MVC framework as they are in any other project.
For the example, I’ll check that my GetGalleries method returns a non-null result:
public class GalleriesModelTest
WebGalleryDataContext db = new WebGalleryDataContext();
public void GetGalleries_ReturnsAllGalleries()
List<Gallery> galleries = db.GetGalleries();
Okay, that fails; which is what it is supposed to do—since my GetGalleries method doesn’t exist yet. I want the methods to show up in side of my existing model and, thankfully, the LINQ-to-SQL classes are partial classes.
If I add a new class called WebGalleryDataContext (the same as the existing DataContext) under my model and mark it as partial, the methods will merge across classes to create a single class. It is a brilliant and extremely useful concept for extending existing classes like those created by the DBML generator.
Our GetGalleries method looks like the code snippet below. We’ll use that to build up on our Index action on the galleries controller.
public partial class WebGalleryDataContext
public List<Gallery> GetGalleries()
With that done, we’re ready to tackle creating a view and attaching some data to it—or creating an actual web page!
Creating a Gallery List with ViewData
On the first post, I created the new views for the Galleries controller; however, I didn’t do anything with them at that time—simply rendering was enough. Now, however, we want to see REAL data on those pages.
Important: There is a glitch in the current CTP (Dec 2007) of the ASP.NET 3.5 Extensions. The MVC Content View and View pages are not generating .designer files—and therefore the code behind cannot access the controls on the page. To fix this, when you create a new view that will have .NET controls on it, right click the view and click “Convert to Web Application.”
I’m not going to debate whether or not you should use strict HTML and leave code-behind out of the equation OR use ASP.NET controls and carefully monitor your own code to ensure that you’re only writing presentation logic. That’s each developer’s individual battle (or with their boss).
In my case, I see value in both. Simple iterations and layouts lend themselves well to what, quite frankly, looks like legacy ASP code. More complex data views, forms, and such lend themselves to controls and code-behind. That’s my opinion.
For our Galleries/Index action, I’d like to list out the galleries, using the GetGalleries method we created earlier, and create HTML links to Show() the gallery. To do this, use simple inline code. Why? I’m not formatting any specific data; simply generating an unordered list of links. There’s no reason to “over code” it.
<% foreach (Gallery gallery in ViewData)
(i => i.Show(gallery.Name), gallery.Name)%>
<% } %>
In our code-behind, we need to do two things.
First, tell our page what type of ViewData to expect. This is done by modifying the generic base class that each MVC View Page inherits from. In this case, we’ll pass a List<Gallery> object to the view, so that’s what we need to tell it to expect.
Second, if we plan to use any server-side controls (ASP.NET controls) on our page, we need to add a Page_Load() method to the page. Add this inside the partial class declaration of your page.
public void Page_Load()
Notice: At the time of this writing, I can’t get the ActionView to accept an Eval(“Value”) statement. I’m honestly not sure why. As soon as I figure it out (or if someone tells me ), I’ll post up an update.
Save and run, and we get:
Creating the Photo Gallery with ViewData
For the last example, we need to implement a third controller, the WebFilesController. For a lack of better names at the time, WebFiles are the domain objects for the photos, documents, and other… uhh.. web files stored within each gallery.
This Controller, however, won’t specify any views—since we’re simply rendering out binary objects (photos, documents, etc).
Information: Quite honestly, I’m not sure if the “viewless” controllers are intended or not—it works. I’m sure someone will come along and note what we “should” and “shouldn’t” be doing for the sake of best practices in the future.
public void Show(int id)
WebFile webFile = db.GetWebFileById(id);
Response.ContentType = webFile.ContentType;
The result allows me to call /WebFiles/Show/16 and the response pipes out the image. I’ve also created a Controller Action called ShowThumbnail to generate a thumbnail and return it to the page in a similar fashion.
Now that we can generate thumbnails, we’re ready to create our gallery.
On the Galleries/Show.aspx view, let’s add a quick server-side control—the DataList. I like the DataList control because it allows me to control the number of columns displayed, custom styles, and formatting.
<asp:DataList runat=”server” ID=”WebFileList” RepeatColumns=”3″ Width=”100%”>
<p style=”text-align: center;“>
src=”/WebFiles/ShowThumbnail/<%# Eval(“Id”) %>“
alt=”<%# Eval(“Name”) %>“ />
<%# Eval(“Name”) %>
As I mentioned earlier, the MvcToolkit’s ActionLink and Image helper methods aren’t working for me whenever I use the Eval statements—I’m still researching why, but, for now, this works out quite well.
Important: Don’t forget to set the ViewPage parameters on the MVC View’s code behind and, if you use .NET controls, add the necessary Page_Load event and control parameters.
For this page, I’ll need to attach the ViewData to the DataSource property of our DataList. Once I’ve specified the type of object in the ViewPage, full Intellisense picks up—and I can specify ViewData.WebFiles since ViewData is a Gallery object and WebFiles (through the relationship in the tables) are child records of a gallery.
public partial class Show : ViewPage<Gallery>
public void Page_Load()
WebFileList.DataSource = ViewData.WebFiles;
Our final product looks something like:
This is the second post in the series discussing new web development frameworks that have come out in the past few weeks and revolve around the Microsoft .NET development platform.
Microsoft .NET MVC Framework
The .NET 3.5 Extensions Package that just hit CTP added in a bit of new functionality—the Microsoft .NET MVC Framework. ScottGu has several great posts about the MVC and a neat four part exercise on connecting to the Northwind Database and hashing out a product list.
For those unfamiliar with the MVC Framework, you may already be doing this to a degree. The MVC (modal-view-controller) pattern is an architectural pattern used to separate the entity model of your application (the business objects and logic or the modal) from the user interface (the view) through the use of a controller. Be careful not to confuse the model as strictly a data model—it’s more than that, it’s the model of your both the data itself and the business logic that goes with it. With this, it’s possible to totally remove the presentation aspect of your design and implement tests more efficiently.
Important: For more of a historical view of the MVC pattern, check out this article. It’s pretty interesting (even though it’s in Java, which is where I first learned of the MVC pattern).
For my example, I’m going to work on implementing a new version of the WebStorage web site (
). I’ve got a v3.5 standard prototype in the works, but this will help demonstrate both how a working application goes together with existing data AND how the tools work (plus, I’m really tired of demoing with the Northwind or AdventureWorks databases).
I’ve broken this up into several parts I’ll be writing over the next couple of days. Quite frankly, the posts ended up being REALLY long. I had planned to do a screencast of this (and still might)—I just need to get my hands of Camtasia (maybe Santa will come early).
These posts also take into consideration you’ve:
- Read the ReadMe’s with the extensions…
- Read through ScottGu’s blog posts on the MVC Framework.
It’s not productive for me to totally reproduce what Scott has—so I’ll be taking in bits and pieces from everywhere. I plan to include a bit more on unit testing (with Rhino Mock examples from Phil Haack), the odds and ins that have come up in creating the MVC apps, and how this compares to WebForms.
Warning: The Extensions Package is currently a preview—there are known glitches (which I’ll note when I find them) and general strangeness that can occur. I don’t recommend pushing out a full MVC project to production right now, but it doesn’t hurt to experiment and become familiar with it. Also, when in doubt, check out the MVC Forums on asp.net. I’ve been reading through them daily and found several very helpful posts.
Creating a MVC Project
The best place to start is by creating a new “ASP.NET MVC Web Application and Test” solution. This template adds both the MVC Web Application and a Test application into your project—with the correct libraries and dependencies already added.
The downfall to this template? Everything is named MvcApplication—kinda annoying that it doesn’t pull up the name of the solution and go with that—a changing that will hopefully be implemented soon.
The default solution structure is pretty simple—folders for each component of the project.
Important: Before we get started, I recommend setting a “Specific Page” start action for your project? Why? We want to be sure we invoke the MVC framework—not specific .aspx pages when we’re testing our application. This way, we can specify a “path” and each time we debug, we’re good to go with that path. As you can see below, since our application will have a default path of / (that, as we’ll see, / routes to our Home controller) , we’ll use that. To get to this screen, right click your Web project | Properties | Web tab.
Preparing Our Default Routes
To start off, I really like how Phil Haack has setup routing—by pulling the routes out of the Global.asax and dropping them into a new class called RouteManager that sits in the root of the web application project.
Before I found this post, the whole idea of cluttering up the Global methods really smelled to me. Good call. It’d be great if they’d simply add this to the templates.
public static class RouteManager
public static void RegisterRoutes(RouteCollection routes)
Type defaultRouteHandler = typeof(MvcRouteHandler);
Url = “[controller]/[action]/[id]“,
Defaults = new
action = “Index”,
id = (string)null
RouteHandler = defaultRouteHandler
// IIS 6.0 Router
Url = “[controller].mvc/[action]/[id]“,
Defaults = new
action = “Index”,
id = (string)null
RouteHandler = defaultRouteHandler
Url = “Default.aspx”,
Defaults = new
controller = “Home”,
action = “Index”,
id = (string)null
RouteHandler = defaultRouteHandler
As an addition, I have added a defaultRouteHandler to my RouteManager. Typing “typeof(MvcRouteHandler)” over and over again and, maybe in the future, changing it is a bit annoying. My hope is that the attribute goes away and/or becomes optional. I shouldn’t need to specify the default value.
Now, in the Global.asax, I can simply call
to prepare my routes and the same in my unit tests—cool.
Preparing our Master Page
I’ll get into formatting a bit later (or I’m sure you can come up with designs FAR better than mine), but the Shared MasterPage does need a bit of work.
To open the shared master page, which is essentially a shared master “view” page, look under Views | Shared.
The Master Page controls our style sheet, navigation, and headers—but you can rip these out and put your own in… you are not limited to the master page provided. It works well, however, for demonstration purposes.
By default, our menu has two options—Home and About Us.
<li><a href=”~/” runat=”server”>Home</a></li>
<li><%= Html.ActionLink(“About Us”, “About”, “Home”) %></li>
I prefer things to be consistant and don’t like the hardcoded URL left in there; we can easily replace it with an Html.ActionLink. (Click here to read more about the ActionLink extension method). In addition, we’ll need one more: the Galleries home page.
The order of the ActionLink parameters is “Link Text”, “Action”, “Controller”. We don’t have a Galleries controller (or an Index action in that controller) yet, but we’ll get there.
<li><%= Html.ActionLink(“Home”, “Index”, “Home”) %></li>
<li><%= Html.ActionLink(“About”, “About”, “Home”) %></li>
<li><%= Html.ActionLink(“Galleries”, “Index”, “Galleries”) %></li>
If we view our page in the browser and click Galleries, we can verify that the link is indeed trying to send us to /Galleries (and we haven’t implemented that yet).
Implementing a New Controller and View
We’re ready to implement a new controller and view for our gallery application. This new controller will handle viewing our galleries and their contents.
Most likely, we’ll have two actions and according URLs will be:
- Index (showing a list of galleries)
Looking at the Show action of our Galleries controller, you can see the route pattern we created above. [Galleries]/[Show]/[##] == [Controller]/[Action]/[Id].
To create a new Controller, right click on your Controllers folder and Add New Item.
As you can see, there are several new MVC file templates available. This time, we want a MVC Controller Class. I’ll name it GalleriesController.
Important: It’s important to note that naming conventions are very important with the MVC Framework templates. Since I’m naming the controller GalleriesController, the framework will automagically assume that the views for this controller are in the “Galleries” directory and attempt to find them there. Be sure your names for ___Controller match the file heirarchy of your views.
Our new GalleriesController contains one method: Index. The Index method is typically our default page action and, according to our routing rules, is the action that fires if we browse to /Galleries or /Galleries/Index or /Galleries/default.aspx.
We want to render a new View, which we’ll create in a moment, called Index. We’re not passing any information to the new view (we’ll get to that later)—simply rendering it.
public void Index()
This creates a link between an action in a controller and a specific view to be rendered. Remember, as I noted earlier, “Index” needs to be in a specific location—in this case, under /Views/Galleries.
I’ll create a new directory under Views called Galleries to hold the Galleries view templates. Inside this directory, I want to “Add a New Item” again; however, this time, add a MVC View Content Page.
The Content Page gives us the option to specify a MasterPage, which we want to select /Views/Shared/Site.Master. Be sure not to choose MVC View Page as you’ll simply get a standard page and need to add in the placeholder information manually.
When finished, the Views heirarchy should look similar to the image on the right.
Again, it’s important to keep the Views matching the name of the controllers. If you have a controller called CustomersController; you’d have a Views directory called Customers.
After adding in a quick header to the Index page, we can browse to our site, click Galleries and see our new view page.
Now, we’re ready to add the galleries catalog and begin pulling in some data. We’ll do that next time!
Download : .NET 3.5 Source Project
The ModalPopupExtender is one of my favorite AJAX extenders. It provides a slick UI experience for little overhead. The most difficult part of implementing the extender is recreating the popup panels over and over again for similar tasks.
To rectify this and create a “template” for use in our organization, I created a custom composite control that encapsulates
- Creation of the AJAXToolkit’s ModalPopupExtender,
- Creation of a Panel control,
- Styles the Panel according to pre-set styles or custom styles,
- Ties the Panel and Extender together,
- Passes various events to the common UI elements of the popup,
- Allows the popup’s events to be visible to other controls on the page.
As it stands, this is a prototype and needs quite a bit more tweaking before I’d put it into production; however, it stands as a fun project to tinker with. This will be an ongoing project and as I finish or update various aspects of the control, I’ll post up the changes. I’m also interested in feedback and ideas for improvement.
The Control’s Structure
Here you can see the properties that are configurable with the control as well as the public and private methods and events. I’ve also included the PopupStyle enumeration which prestyles the popup.
- _Element_Style (BodyStyle, ContainerStyle, etc) properties accept strings from your embeded or attached CSS.
- DefaultStyle is linked to the PopupStyle enumeration (and may lend itself to rename eventually). The example I showed above is the YUI theme which uses Matt Berseth’s YUI Css. I’ve also included a “Clean” theme similar to what we use in our projects at work. I hope to grow the collection of themes as time progresses.
- OnOkClick, OnCancelClick, and OnCloseClick methods link to the corresponding buttons on the popup and throw the like named events. By default, if these events are not tied to methods in your project, they simply close the popup window.
- BodyText and HeaderText are the two primary configuration elements of the popup.
Here’s an example including shaky arrow lines matching the properties to their relative location. For examples of the Style (ContainerStyle, BodySTyle, HeaderStyle, etc) check out Matt Berseth’s YUI example—the CSS is the same (Thanks Matt for an EXCELLENT job and template to follow!)
Using the control on your .NET page is quite simple. Remember, as with the normal ModalPopupExtender, your ModalPopupTemplate control must be in the same UpdatePanel as the TargetControl that activates it. You’ll also need to ensure that EnablePartialRendering is set to true for your ScriptManager control.
<asp:UpdatePanel ID=”updatePanel” runat=”server” UpdateMode=”Conditional”>
<asp:Button ID=”btnTrigger” runat=”server” Text=”Show Popup” />
HeaderText=”Load Product Information”
BodyText=”Are you sure you want to load the product information?”
How it Works
I’ll dig into how it works in the next posting—being somewhat late and being very sick and on some good drugs isn’t conducive to lots of writing. If you can’t wait, download the source and go—it’s pretty well code commented.