Site Meter
 
 

Monthly Archives: November 2008

jQuery Ajax uploader plugin (with progress bar!)

Do your web applications ever involve letting the user upload a file? If so, how’s the end-user experience: do you show a nice progress bar during the upload, or do you just leave the user waiting for minutes, with no clue when (if ever) the upload will complete?

Please show a progress bar, otherwise users will be justified in hating you. Check out this video to see one way it can work:


If you’re not seeing a video here, your feed reader is hiding it. View this post in a browser to see the video.

Those of you who attended my ASP.NET MVC talk at DDD7 last weekend might recognise this ;)

To create this behaviour, I implemented a simple jQuery plugin that replaces normal <input type=”file”/> elements with funky Ajaxy asynchronous uploader widgets. Behind the scenes, it uses the excellent SWFUpload library. All the clever stuff is in SWFUpload; all I did is set up the progress bar / cancellation behaviours, and make it easier to use if you’re already using jQuery.

Notice that it still works if the user doesn’t have JavaScript running in their browser. It gracefully degrades to “traditional” <input type=”file”/> behaviour. This is known as progressive enhancement or unobtrusive JavaScript.

Download

Here are all the files you need to accomplish this: Download jQuery-asyncUpload-0.1.js.

Setup instructions

Uploading files via Ajax, by nature, involves setting things up both on the server and on the client. The most reliable way to get this working successfully in your own app is to download the demo ASP.NET MVC project (see the end of this post) and copy the relevant aspects of its workings into your own app.

Nonetheless, here is an outline of the steps needed to get jQuery-asyncUpload-0.1.js working in your app, assuming you’ve already got jQuery in there:

1.  Add jQuery-asyncUpload-0.1.js, swfupload.js, and swfupload.swf to your project. In an ASP.NET MVC app, you might like to put these in /Scripts.

2.  Add script tags to reference the JavaScript files.

<head>
    <!-- Adjust the file paths as needed for your project -->
    <script src="/Scripts/jquery-1.2.6.min.js"></script>
    <script src="/Scripts/swfupload.js"></script>
    <script src="/Scripts/jquery-asyncUpload-0.1.js"></script>
</head>

3.  Add an old-style HTML file upload control to one of your pages:

<input type="file" id="yourID" name="yourID" />

4. Add a jQuery statement that replaces this file upload control with an asynchronous uploader when JavaScript is available:

<script>
    $(function() {
        $("#yourID").makeAsyncUploader({
            upload_url: "/Home/AsyncUpload", // Important! This isn't a directory, it's a HANDLER such as an ASP.NET MVC action method, or a PHP file, or a Classic ASP file, or an ASP.NET .ASHX handler. The handler should save the file to disk (or database).
            flash_url: '/Scripts/swfupload.swf',
            button_image_url: '/Scripts/blankButton.png'
        });
    });
</script>

These options are explained later in this blog post. You must make sure to correctly reference the location of swfupload.swf, and put a button image wherever button_image_url specifies.

5. Add some CSS rules to style the progress bar. I’m using the following, though bear in mind it has some nasty hacks to make IE do an inline float properly. CSS gurus might structure this more cleanly.

DIV.ProgressBar { width: 100px; padding: 0; border: 1px solid black; margin-right: 1em; height:.75em; margin-left:1em; display:-moz-inline-stack; display:inline-block; zoom:1; *display:inline; }
DIV.ProgressBar DIV { background-color: Green; font-size: 1pt; height:100%; float:left; }
SPAN.asyncUploader OBJECT { position: relative; top: 5px; left: 10px; }

5. At this point, check you have something working. The visitor should now be able to select a file to upload, and should immediately get an alert box saying “Error 404” – that’s because you’ve configured the control to do an asynchronous upload to /Home/AsyncUpload, but your web app probably doesn’t have anything at that URL.

Also, if you use FireBug to inspect the DOM, you’ll see that your <input type=”file” /> has been dynamically replaced with the following:

<span class="asyncUploader">
   <div class="ProgressBar" style="display: none;">
	<!-- This is the progress bar itself - you can style it with CSS -->
   </div>
   <object type="application/x-shockwave-flash" ... >
       ... SWF config here ...
   </object>
   <input type="hidden" name="yourID_filename"/>
   <input type="hidden" name="yourID_guid"/>
</span>

Those two hidden inputs let you keep track of any file that was asynchronously uploaded.

6. Work on your web app so that it *does* handle file uploads to /Home/AsyncUpload (or whatever URL you’ve configured in step 4). The handler should save the uploaded file to disk, then return a unique token, such as a GUID or filename, to will identify the file you just uploaded. See the demo project for a simple way to do this using ASP.NET MVC.

7. When the containing form is finally submitted, check whether a file was sent with the request. This will happen if the user doesn’t have JavaScript enabled, as they’ll revert to traditional uploading behaviour. Also check for the hidden inputs called yourID_guid and yourID_filename – these will be populated if the visitor *does* have JavaScript enabled, and reflect any file that was uploaded asynchronously.

Further configuration

The asynchronous uploader plugin has plenty of properties you can configure in step 4 above:

Property Meaning Example
flash_url Location of swfupload.swf “/Scripts/swfupload.swf”
upload_url URL of the handler to which files will be asynchronously sent

Important: This is not the name of a directory or file on your server’s hard disk. It is the URL of a handler, such as an ASP.NET MVC action method, or a PHP page, or a classic ASP page, or an ASP.NET WebForms .ASHX handler, which will receive the file. It remains your job to implement such a handler and then save the incoming file to disk or database. For an example, see the ASP.NET MVC demo project at the end of this blog post.
“/Home/AsyncUpload”
file_size_limit Files above this size will be rejected before uploading even begins “3 MB”
file_types “Select files” popup will only show files of this type “*.jpg; *.gif”
file_types_description “Select files” popup will use this caption to describe the selectable file types “All images”
button_image_url Location of an image to be used for the “Choose file” button “blankButton.png”
button_image_width, button_image_height Dimensions of “Choose file” button 109
button_text Text that appears on the “Choose file” button "<font face=’Arial’ size=’13pt’>Choose file</font>"
disableDuringUpload Elements matching this jQuery selector will be disabled while an upload is in progress (useful to prevent form submission during async upload). “INPUT[type=’submit’]”
existingFilename Prepopulates the control with the name of a file already uploaded (useful when retaining state across multiple posts) “somefile.zip”
existingGuid Prepopulates the control with the arbitrary unique token you’ve given to a file already uploaded (useful when retaining state across multiple posts) “ec42555e-bfe7-45b0-87bf-36b1299f0398”
existingFileSize Prepopulates the control with the size, in bytes, of a file already uploaded (useful when retaining state across multiple posts) 548293
debug Turns on SWFUpload’s debugging console true

Demo project

The easiest way to understand all this is to check out a completed implementation. Here’s one written for ASP.NET MVC.

To compile and run this, you’ll need Visual Studio 2008 and ASP.NET MVC Beta. It saves asynchronously-uploaded files to the folder ~/App_Data/Files, giving each one a GUID as a filename. When you finally submit the form, it simply displays the filename and GUID of whatever file you uploaded. In a real app, you wouldn’t display the GUID to the end user, but would instead just use it to locate their file later.

kick it on DotNetKicks.com

Speeding up communication between Firefox and Visual Studio’s built-in web server

image Has anyone else noticed that, during development, if you use Firefox to browse a site hosted in Visual Studio 2005/2008’s built-in web server (WebDev.WebServer.Exe), it’s really, just, so, excruciatingly … slow?

Oh it’s fine if you use Internet Explorer. Or Chrome for that matter. But every request issued from Firefox to WebDev.WebServer.Exe seems to have about a full second of lag added. If your pages host multiple images and CSS files, it’s intolerable.

It works on your machine

What’s that you say – you haven’t experienced this problem? Lucky you! I’ve asked other developers about this, and they all claim it’s fine on their machine too. And a quick Google search doesn’t turn up any other reports of the issue.

But I’ve had four different physical development PCs in the last couple of years, running three major different OS versions, two major different versions of Visual Studio and two major different versions of Firefox, and still have been wading through treacle every single time! Firefox is way faster communicating with servers on the other side of the planet than with this particular web server hosted on the same PC. What gives?

The conspiracy theorist in me even wondered if some evil person in Microsoft had hard-coded this behaviour into VS, intending to discourage web developers from working exclusively in Firefox. I confess I even changed my Firefox UserAgent string to match MSIE, but that didn’t help.

The solution

Finally! It turns out to be something to do with IPv6. I don’t know whether the problem is in Firefox or in WebDev.WebServer.Exe, but disabling Firefox’s IPv6 support is like cutting off the ball and chain. Suddenly, communication with the local server is back to the blistering pace you’d expect.

All you need now is instructions for disabling Firefox’s IPv6 support.

App Areas in ASP.NET MVC, take 2

So the discussion continues: How do you partition an ASP.NET MVC application into separate “areas” or “modules” (e.g., blog module, e-commerce module, forums module), then compose a finished app from those areas or modules?

Phil Haack, ASP.NET MVC program manager, just posted a prototype application structured in that way. It’s a really neat solution, and overcomes a number of issues that others, including myself, have experienced when trying to do it previously.

In this post, I’m going to take Phil’s prototype and tweak it in a few ways to my liking. That’s not to say that there’s anything wrong with his design, but only that I want to throw in some extra ideas that might make it even slicker in some cases.

How it works

The mechanism as given comes in two parts:

  • Routing configuration: There’s an extension method on RouteCollection called MapAreas() which lets you register a URL pattern for multiple areas. You pass it a URL pattern, a controller “root namespace”, and an array of area names. It prefixes “{area}/” to your URL pattern, and for each area name, it registers a route entry that targets controllers who live in the namespace given by: 
    (root namespace + “.Areas.” + area name + “.Controllers”)
  • View engine: There’s a special view engine called AreaViewEngine that uses a built-in convention to look for view templates in an area-specific folder.

Don’t worry if you don’t understand this. If you download Phil’s prototype and try it, you’ll find it’s all straightforward enough.

It works very nicely, and it uses a clever trick with route defaults and constraints so that when you’re generating outbound URLs, you can link to controllers/actions in any area by specifying the area name in your Html.RouteLink() call, or you can link to controllers/actions within the same area using a normal Html.RouteLink() call that doesn’t specify any area name.

Suggestions for enhancement

I’m not going to touch the view engine part of the prototype at all. All I want to achieve is a configuration system that’s slightly more natural (for me) and a modified set of conventions and rules that are a bit more flexible. Here’s how I’d like a simple areas routing configuration to look:

// Routing config for the blogs area
routes.CreateArea("blogs", "AreasDemo.Areas.Blogs.Controllers",
    routes.MapRoute(null, "blogs/{controller}/{action}", new { controller = "Home", action = "Index" })
);
 
// Routing config for the forums area
routes.CreateArea("forums", "AreasDemo.Areas.Forums.Controllers",
    routes.MapRoute(null, "forums/{controller}/{action}", new { controller = "Home", action = "Index" })
);
 
// Routing config for the root area
routes.CreateArea("root", "AreasDemo.Controllers",
    routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" })
);

… and here’s an example of a very slightly more complex configuration:

// Routing config for the blogs area
routes.CreateArea("blogs", "AreasDemo.Areas.Blogs.Controllers",
    routes.MapRoute(null, "SpecialUrlForPosts", new { controller = "Home", action = "Posts" }),
    routes.MapRoute(null, "blg/{controller}/{action}/{id}", new { action = "Index", controller = "Home", id = "" })
);
 
// Routing config for the forums area
routes.CreateArea("forums", "AreasDemo.Areas.Forums.Controllers",
    routes.MapRoute(null, "myforums/SecretAdminZone/{action}", new { controller = "Admin", action = "Index" }),
 
    // Equally possible to construct routes using "new Route()" syntax too
    new Route("myforums/{controller}/{action}", new MvcRouteHandler()) {
        Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index" })
    }
);
 
// Routing config for the root area
routes.CreateArea("root", "AreasDemo.Controllers",
    routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" })
);

The point to notice is that each area is configured independently of the others, which for me feels more natural than defining a single URL pattern that for some reason applies to all areas.

It turns out to be dead easy to make it work like this. All you need is this simple CreateArea() extension method:

public static void CreateArea(this RouteCollection routes, string areaName, string controllersNamespace, params Route[] routeEntries)
{
    foreach (var route in routeEntries)
    {
        if (route.Constraints == null) route.Constraints = new RouteValueDictionary();
        if (route.Defaults == null) route.Defaults = new RouteValueDictionary();
        if (route.DataTokens == null) route.DataTokens = new RouteValueDictionary();
 
        route.Constraints.Add("area", areaName);
        route.Defaults.Add("area", areaName);
        route.DataTokens.Add("namespaces", new string[] { controllersNamespace });
 
        if (!routes.Contains(route)) // To support "new Route()" in addition to "routes.MapRoute()"
            routes.Add(route);
    }
}

Apart from that, there’s no difference from the original prototype (well, you can delete the original prototype’s MapAreas() and MapRootArea() methods: they’re no longer used).

Benefits

How does this differ from the original prototype? What conventions have changed?

  • URL patterns are no longer forced to start with the area name (though they can if you want). Within a single area, you can have some URLs that start with the area name, and some that don’t.
  • In fact, URL patterns are now totally independent of area names, which means your area names can simply be internal code words for software modules, never seen by the public. If that’s what you want.
  • Controller namespaces are independent of area names, and they don’t even have to be constant within a single area. Pick your own convention and follow it.
  • You can  configure each area’s routes in a separate block of code, using a sweet DRY syntax, which for me feels more natural than having a single method call that registers routes across all areas.
  • You don’t need any special code or configuration for the “root” area – that’s just another area like the others, usually just with shorter URL patterns.
  • You can keep using the familiar routes.MapRoute() and new Route() ways of building route entries, merely wrapping up groups of them in CreateArea() calls. No significant new API.

Apart from this, it uses almost exactly the same mechanism as in the original prototype.

Drawbacks

If enforcing conventions is your thing, then you might not appreciate the extra flexibility that comes with these changes.

Also, when you do cross-area links, you have to be careful to specify a controller name and not just assume the default controller will be used. Otherwise, the generated URL might reuse the current request’s controller name, which might not even exist in the destination area. This is because of an obscure technicality in how URL generation works (it doesn’t affect Phil’s original design because of how he requires all URLs to start with an “{area}” segment).

This isn’t worth explaining in detail – all I’ll say is that the solution is simply to make sure your cross-area links always specify a controller name. Of course, you almost certainly should be doing that anyway, because it would be very weird to link just to an action name on a different area without being clear about which controller hosts that action.

Summary

ASP.NET MVC continues to impress me with its flexibility. If you want to structure your app in terms of “modules” or “areas”, it doesn’t take much code to enable it. Phil’s approach to areas is the neatest I’ve seen so far. Personally I like the tweaks I’ve suggested above, but it’s subjective and you can do things your own way.