iPhone: it's not really a digital convergence device

It finally gelled in my head what bugged me about the iPhone's lack of simultaneous processing.  It's not truly a digital convergence device.  It's a sequential task device.

A digital convergence device is a device that does lots of things: MP3/FM music player, alarm clock, cell phone, web browser, calendar, address book, GPS-enabled map, pedometer, etc.  The purpose of such a device is to do all of these things, not each of these things.  If I can't do them all simultaneously, it isn't a convergence device, it's a sequential device.  The iPhone is exactly that: a sequential task device.

If I've decided I'm going to IM, I can't do anything else with the phone -- I can't browse the web, can't check on news feeds, can't check email.  The device is useless to me until I get an IM.  If I wanted an IM-only device, I'd have bought one.  I bought a digital convergence device.  The same could be said of any app that receives data: Skype, Google Voice, Email, RSS feeds, Facebook sync, texting services, RTMPGs, etc, as well as anything that monitors anything like Google Latitude, a pedometer app, navigation app, etc.  Anything I expect to alert me to something or to keep track of something either needs to "push notification" me, or I forfeit all other features of this device as I use it for that task.  If it goes the push notification route, the logic must be in the cloud, not on my device, can't be peer-to-peer, and can't get real-time status call-backs -- e.g. it can't auto-detect where I am and update my progress.  As an extreme example, I can't count the number of times I was playing some random game and walked around for a while looking for a clock so I didn't need to kill my game.  My convergence device was in "single task" mode.

The notable exceptions to the single-use mode are all Apple apps: phone, iTunes, and alert apps such as calendar and text alerts.  I can get a call which immediately and irrevocably halts anything I'm doing (including upgrading OS versions), and I can listen to iTunes music while I do a few other things.  But what if my music player of choice is Pandora?

I think you see where I'm going with this.  The iPhone clearly isn't a digital convergence device.  Neither is the iPad.  It is clearly a sequential task device, a data snacker device.  Pick the function you want it to do now, and it'll do great.  Want to do 2 things at once?  Well of course, buy two of them.  Want to get alerted when something happens?  Buy a specialty device too.  Um, I think I'll pass.

iPhone: the rise and fall of the latest Apple gadget

The iPhone.  It's a wonderful device.  It was a game changer.  3 years ago when it was released, it revolutionized the phone landscape.  For the first time, people had a portable digital convergence device.  (Ok, maybe it wasn't the first, maybe it wasn't the best, maybe it was just targeted at regular people instead of corporate users.)  For the first time, we could walk around with a web browser, a calendar, a music player, an address book, and really manage our lives on the go.  (Ok, I'll grant that BlackBerry defined that niche for business users, but the iPhone made it cool and made it work for consumers.)

I'm hesitant to sit on the bleeding edge.  I waited until Service Pack 1 to get my iPhone 3G.  It was nearly 2 years ago.  It was a game changer for me too.  At random times in random places, I could check email, surf the web to answer the nagging question, create calendar entries, or just horse around.  I could pull up a map of where I am and where I wanted to go, so the preparation for travel wasn't as urgent.  I could schedule stuff on my calendar, so no more wads of post-its hoping I guessed right.  And reading email in the 2 minutes standing in line or on the walk home from dropping kids off at school is awesome.  It truly revolutionized my world for the better.

Fast forward 3 years for Apple, nearly 2 years for me.  I waited with baited breath at every Apple announcement for the thing that'd keep the iPhone cool or for them to switch to a new carrier.  (The tag-line for the iPhone I've heard not a few times is, "The iPhone is so cool it almost makes up for being stuck on AT&T.")  In that time, they've started to back-fill some missing features, and made subtle improvements, but they've really never leaped out and grabbed me again.

The following features were instantly missing from my experience, and with few exceptions, Apple really never delivered:
  • OTA Syncing
  • contact and calendar categories
  • turn-by-turn voice navigation
  • copy & paste
  • an IM and VOIP client that's always on (e.g. I'll still get new IMs or receive VOIP calls if I switched over to read my email)
  • 3rd party data on maps (e.g. "map all my contacts" or "where is the closest gas station on my route" or "does Acme, Inc. have an office near me?"  I ranted about this previously -- that I want to shove a kml file into maps.)
  • OpenVPN / RDP client
  • disable auto-rotation based on the accelerometer -- so I can read in bed
Ok, I'll grant that some of these things have come with third-party apps.  Some have come as jailbreak apps.  Some are just not there.

And I get that I'm hardly a typical user.  Most users looking for "IM client" on a phone would point me at text messaging.  (Yeah, but I can't text into an MSN IM contact.  And if a Skype contact IMs me back but I'm sending an email, I never get it.  Yeah, I know Trillian Astra is "almost there".  I've been waiting on them for as long too.)


Meanwhile, over the last 3 years since the iPhone changed the landscape, Android and most recently Windows Mobile 7 have come into view.  Neither is as polished as the iPhone was at launch.  (I've not seen a Win 7 phone, but I've seen a good deal of Android phones.)  Android only recently got pinch-to-zoom, and really only on Nexus One, and the Droid's map application.  But I see a lot more potential in these devices than in the iPhone.

For instance, me and my family were on vacation in California.  We were caravaning with friends on an unfamiliar stretch of highway.  We popped open the Droid, put in our destination in the voice guidance app, and went back to the home screen.  When something of interest popped up in a friends' cars, they'd text us or tweet.  As the perfect example, my wife was on the web looking up something, the navigation was telling me where to turn, and a text came in.  It all happened simultaneously.  It just worked.

I've played with the Droid pretty extensively since.  I can push the search button, and pull up a web page or a contact or directions.  The back button is just truly awesome -- getting me back a web page or back into the previous app or back a page in the app.   It just works.  The syncing is awesome because it just works.  I'm very good at forgetting day-to-day stuff, and so I often find I haven't sync'd my calendar in a while, and when I do, I double-booked myself.  There's no android sync procedure -- it just does it.

And don't get me started on the iPhone carrier lock-in.  I probably would've gotten a 3GS if my carrier of choice was announced last July.  As it stands, I'm the only member of my circle of mobiles that can't use free mobile-to-mobile minutes ... because I'm stuck in AT&T -- say nothing of the service.


When Android finally got pinch-to-zoom, it was a done deal.  I'm sold.  As soon as my AT&T contract is up, I'm sailing away on Android.  It'll let me use it the way I want.

Rob

VS's Cassini (Web Server) + Fiddler = Remote testing

From time to time, I have a need to debug a web site from a browser not installed on my current machine.  Safari/Mac or phone browsers are the quintessential example.  Granted, if it happens server-side, it happens for all browsers, and if it happens client-side, debugging server-side may not help.  But there's that one random time every now and again when this becomes quite important: debug server, client isn't on localhost.  Perhaps it's easier to replay the request from the browser where the problem happened rather than figure out how to simulate it locally.  Perhaps it's a weird combination of race conditions that make it happen.  Perhaps it's how the cookies built up that led to cruft not easily reproducible elsewhere.  Alas, being able to debug the server from a non-local client comes in mighty handy now-and-again.

As we all know, Visual Studio's built in web server (originally based on Cassini) is hard-coded to only bind to loopback.  If I had my way that wouldn't be the case, but here we are.  Traditionally, we'd need to install our site in IIS, attach to the w3svc service, and debug that way.  (Even if you're not on a 64-bit box, you can empathize with how intense that is.)

Fiddler is an awesome tool for watching network traffic flow between browser and web server.  In the end, everything you send between server and browser is either HTML, CSS, or JavaScript (or binary content like images and plug-ins), so watching it happen is incredibly insiteful.  We'll not use Fiddler for that here, but I highly recommend it for that work as well.  (There's times when watching the traffic yields insights quite difficult to see any other way.)  Alas, today we'll just mis-use it as a proxy server that can forward traffic back and forth from a LAN address to a loopback address.

Here's the steps:

1. Download and install Fiddler from www.fiddlertool.com on the same machine as Visual Studio.

2. Tools menu -> Fiddler Options -> Connections tab: click on "Allow remote computers to connect", and push ok.  (While I'm in here, I usually turn off IPv6 in the General tab as that's another no-no in VS's version of Cassini.)

3.  Rules menu -> Customize Rules.  For me, it opened up CustomRules.js in notepad.

4. Scroll down to the function labeled OnBeforeRequest and add a line kinda like this:

if (oSession.host.toLowerCase() == "192.168.1.5:8888") oSession.host = "localhost:45678";

Replace the first IP with the address of your machine (ipconfig /all from a cmd will give you this), and replace the port at the end with what ever you set / VS assigned to your project.  (Inside Visual Studio, in the project properties window, in the web tab, I always set it to "Specific Port" so I don't have to come in here and futz with it every time VS wants to "help".)

This line says "if the inbound request says it's going to this ip and port (that fiddler is listening to), reroute it to this other ip and port (that Visual Studio's web debugger is listening to).

5. Scroll down a bit further in CustomRules.js to the OnBeforeResponse function and add a line kinda like this:

oSession.utilReplaceInResponse("localhost:45678","192.168.1.5:8888");

You'll also need to change the IP and port here to match the changes above as well, but note they're reversed.

This line says "if the body of the outbound message includes text that routes to Visual Studio's debugger, rewrite it to point to Fiddler."

6. Save this file, restart Fiddler, and try it out.

(You may also have to adjust firewall rules on your development machine to allow inbound TCP connections on port 8888.)

And that's it!  Now you're remote debugging your project from a non-local address.

One final note: ideally, we'd create another rule in OnBeforeResponse that would rewrite headers as well.  I haven't been very successful in doing that, so I haven't included all the ways that don't work.  But if you're sending back a redirect to a full url without this rule, your client will happily time out looking for content on itself.  Why itself?  Because you redirected to localhost.  :D

Rob

Logging and Rewriting Service Calls

I love web services.  I love how easy it is to wire up a JavaScript client either through jQuery or through a ServiceProxy.  I love how I can point a .net client at a service, and I get strongly typed command parameters.  And my latest love of asmx services in C# is the SoapExtension.  A SoapExtension is a class that allows you to inspect outgoing or incoming services to get at the data involved.  I use them for logging service calls (frequency, duration, errors), and for rewriting output (fixed a "double html encoded" bug, remove whitespace, rewrite SoapFaults, etc).

The technique is incredibly awesome, and I can't claim original ownership, but I can claim excellent results.  A few simple steps:

1. Create a class that derives from SoapExtension.

2. Override ProcessMessage, handling the SoapMessage's Stage that you're interested in.

3. Add either a web.config entry (to handle all inbound and outbound service calls), or put an attribute on each WebMethod you want to track.

Voila!  You've got a soap extension.

Here's my ExampleSoapExtension (business logic replaced with TODO to keep it simple):

namespace ExampleNamespace {

    #region using
    using System;
    using System.Data.Linq;
    using System.IO;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Web;
    using System.Web.Services.Protocols;
    #endregion

    public class ExampleSoapExtension : SoapExtension {

        private Stream originalStream; // The original stream
        private Stream internalStream; // Our new stream we chained into place

        public ExampleSoapExtension() {
        }

        // Called the first time the Web service is used if configured with a config file
        public override object GetInitializer( Type t ) {
            return typeof( ExampleSoapExtension );
        }

        // Called the first time the Web service is used if configured with an attribute
        public override object GetInitializer( LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute ) {
            return attribute;
        }

        // Called each time the Web service is used and gets passed the data from GetInitializer() method
        public override void Initialize( object initializer ) {
            // Get the attribute here and pull whatever you need off it.
            //ExampleSoapExtensionAttribute attr = initializer as ExampleSoapExtensionAttribute;
        }

        // The ChainStream() method gives us a chance to grab the SOAP messages as they go by
        public override Stream ChainStream( Stream stream ) {
            // Save the original stream
            originalStream = stream;
            // Create and return our own in its place
            internalStream = new MemoryStream();

            return internalStream;
        }

        // The ProcessMessage() method is called between each step in the server-side process
        public override void ProcessMessage( SoapMessage message ) {

            switch ( message.Stage ) {

                case SoapMessageStage.BeforeDeserialize:
                    // About to handle incoming SOAP request

                    string requestContent = StreamToText( this.originalStream, false );
                    WriteIncomingToLog( message, requestContent );
                    TextToStream( requestContent, this.internalStream, true );
                    break;

                case SoapMessageStage.AfterDeserialize:
                    // incoming SOAP request has been deserialized

                    WriteInputToLog( message );
                    break;

                case SoapMessageStage.BeforeSerialize:
                    // About to prepare outgoing SOAP Response

                    WriteOutputToLog( message );
                    break;

                case SoapMessageStage.AfterSerialize:
                    // outgoing SOAP response is ready to go

                    string responseContent = null;

                    if ( message.Exception != null ) {
                        // Rewrite soap fault as valid soap message with internal error code
                        // Though it might be nice to do this before serialization, we can't inject our own return value easily
                        responseContent = RewriteError( message );
                    } else {
                        responseContent = StreamToText( this.internalStream, true );
                    }

                    WriteOutgoingToLog( message, responseContent );
                    TextToStream( responseContent, this.originalStream, false );
                    break;

                default:
                    throw new Exception( "invalid stage" );
            }
        }

        private static string StreamToText( Stream stream, bool rewindBefore ) {
            if ( rewindBefore ) {
                stream.Position = 0;
            }
            TextReader reader = new StreamReader( stream );
            string content = reader.ReadToEnd();
            return content;
        }

        private static void TextToStream( String text, Stream stream, bool rewindAfter ) {
            TextWriter writer = new StreamWriter( stream );
            writer.Write( text );
            writer.Flush();
            if ( rewindAfter ) {
                stream.Position = 0;
            }
        }

        #region WriteIncomingToLog
        /// <summary>
        /// Write the incoming stream content to the logs
        /// </summary>
        private void WriteIncomingToLog( SoapMessage Message, string Content ) {

            HttpRequest req = HttpContext.Current.Request;

            // TODO: Log various parameters here including the incoming xml and start date

        }
        #endregion

        #region WriteInputToLog
        private void WriteInputToLog( SoapMessage Message ) {

            ServiceInputObject input = null;
            if ( Message.MethodInfo.InParameters.Length > 0 ) {
                try {
                    input = Message.GetInParameterValue( 0 ) as ServiceInputObject;
                } catch ( Exception /*ex*/ ) {
                    input = null;
                }
            }
            if ( input != null ) {
           
                // TODO: Log the message details
               
            }

        }
        #endregion

        #region WriteOutputToLog
        private void WriteOutputToLog( SoapMessage Message ) {

            if ( Message.Exception != null ) {
                // The return object doesn't exist because an exception was thrown
                // RewriteError will create one
                return;
            }

            ServiceOutputObject output = null;
            if ( Message.MethodInfo != null && Message.MethodInfo.ReturnType != null && Message.MethodInfo.ReturnType.FullName != "System.Void" ) {
                output = Message.GetReturnValue() as ServiceOutputObject;
            } else {
                output = null;
            }
            if ( output != null ) {
           
                // TODO: Log the output details
               
            }

        }
        #endregion

        #region WriteOutgoingToLog
        /// <summary>
        /// Write the message in the stream to the log4net log
        /// </summary>
        private void WriteOutgoingToLog( SoapMessage Message, string Content ) {

            string outboundData = GetStreamText( StreamToLog );

            // TODO: Log the outgoing xml string and end date

        }
        #endregion

        #region CaptureError
        /// <summary>
        /// Insure the exception is logged
        /// </summary>
        private Stream CaptureError( SoapMessage Message, Stream stream ) {

            // Log the exception
            Exception ex = Message.Exception;
            while ( ex != null && ex.InnerException != null && ex.GetType() == typeof( SoapException ) ) {
                ex = ex.InnerException;
            }
           
            // TODO: Log the exception
           
            // TODO: Form the output reply and serialize it
            string soapMessage = null;

            // Now send this as the out stream instead of the <soap:Fault> stream we had before
            MemoryStream outStream = new MemoryStream();
            TextWriter writer = new StreamWriter( outStream );
            writer.WriteLine( soapMessage );
            writer.Flush();
            outStream.Position = 0;

            // And send the 200 status instead of 400
            //HttpContext.Current.Response.StatusCode = 200;

            // Here's the content to send instead of what ever you had
            return outStream;
        }
        #endregion

    }

    [AttributeUsage( AttributeTargets.Method )]
    public class ExampleSoapExtensionAttribute : SoapExtensionAttribute {

        public ExampleSoapExtensionAttribute() {
            this.Priority = 1;
        }

        public override int Priority { get; set; }

        // Specifies the class of the SOAP Extension to use with this method
        public override Type ExtensionType {
            get { return typeof( ExampleSoapExtension ); }
        }

    }

}

My current quest: how do I do this in WCF?  Is there such a thing as a "message watcher" or "handler" or "pipeline interceptor"?  All these terms on Google come up blank.  With WCF's new configuration-less services and MS's desire to move on past asmx, WCF seems a foregone conclusion.  For me, this is the last hurdle.

Rob

Desert Code Camp and Desert Code Camp Jr

In the past few years, I've been privileged to learn and network at Desert Code Camp.  When I jumped the fence to teach, it was an honor to learn from y'all as I shared the skills you'd taught me previously.  Many years ago, I began by teaching an intro to CSS and a team-taught SVN class.  Last code camp, I taught Thinking in JavaScript, Intro to jQuery, and Advanced jQuery including building plugins and animations.  I also got to learn about ASP.NET MVC, programming usb gadgets, and unit testing JavaScript -- to name a few.  It was a blast.

For anyone who's not taught the skills and talents you have, take the opportunity to do so.  It is well worth the effort and the skills you develop are invaluable.  Imagine how confident you'll be in the next job interview when you notice the same stage fright that quickly passes when teaching a room full of peers can also dissipate just as fast when you're getting grilled by a potential colleague.  And imagine the awesome resume tick and skill justification when you can document that not only do you have x years doing it, but that you taught it to a crowded room of your peers.

But I digress.  This year I'm taking a different path with Desert Code Camp.  My son often comes with me to Users' Group meetings -- mostly for the 1-on-1 time with me as we drive and the free pizza and stickers.  (At the conclusion of an evening 6 months or so ago, with his collection of treasured swag in hand, he said to me, "Dad, there's a lot of stickers in programming."  :D)  Each of the recruiters also engage him a bit each time, and I'm sure if he played his hand right, he could find an awesome job in 5 or 10 years.  Today, he does pretty well in DreamWeaver if I jump in with hard stuff and "how do you spell ...?" from time to time.  His site is robrich.org/mark.html.  He created the concept when he was 4 as a way to keep track of his favorite sites without the need to ask me where the letter H was on his keyboard every time.  For a 4-year-old, the problem's solution was brilliant!  Lately he's been asking to learn more about the guts of how it works -- hardware, software, marketing, and finance.  An eager mind like his is such a treasure.

Thus, this year, I'm not attending Desert Code Camp as a learning and teaching and networking opportunity.  This year, it will all be about my son.  Desert Code Camp Jr is a trek slated for kids "ages 4 to 18".  (In my opinion, kids over  13 or 15 or so will probably want to take -- or teach -- the other classes though.)  We'll learn about MIT's Scratch -- drag-drop programming for kids, programming Lego Mindstorms, and we'll learn about circuits.  Lunch is generously provided by GangPlank.  Mark explained to me he wants to give me some time to go to a grown-up class too, though I'll defer to him once the excitement kicks in.  The way they've structured it with a small instruction period up front and then a generous experimentation and play time afterward for each topic will be perfect for developing skill and creativity in these bright young minds.

Thus, if you were looking for the Continuous Integration class or the Unit Testing with TypeMock and Ivonna class I hyped, or an encore performance of the Advanced jQuery class, you'll have to wait until next year.  This year, I get to be Dad -- the greatest honor I could imagine.

Rob

Continuous Integration

It's my pleasure tonight to present Continuous Integration: best practices, methodologies, and tools at the SEVDNUG.  Continuous Integration is the process of rebuilding the code in its entirety periodically and automating awkward deployment processes to create a more consistent product.  Tonight we discussed these business cases, the tools and techniques available, and best practices to use when implementing a continuous integration process.

It isn't difficult to get an automated build going.  Really the most difficult part is getting a one-line command to build it.  Be it batch files, calling Visual Studio's command line, or NAnt or MSBuild tasks, once you've got a command-line build, rigging it to a Continuous Integration service like CruiseControl.NET is easy.  Once you can automate this code verification, you'll immediately see benefits in decreased time chasing what broken and who broke it.

The slides and code are available here. CruiseControl.NET is available here, NAnt is available here, and NAntContrib is available here.

** NOTE: For some strange reason, my server wants to gzip zip files when it downloads them, leading to a double-compressed file. If you're downloading the zip files in IE, it'll tell you the file is corrupt. The fault is totally mine, but the file is not corrupt. If you download the file with FireFox, it will come down just fine. If you're an IIS guru and know how to disable gzipping zipped content while enabling gzipping for non-zipped content, please let me know. **

Testing: Methodologies, Best Practices, and Tools

I had the privilege to speak at the SEVDNUG yesterday about Testing. When we typically think of "Testing", we usually use the term "Unit Testing", yet we usually mean "Integration Testing" or "Functional Testing". This was definitely not a demo on how to use NUnit, but rather a specific look at the reasons for testing, the best practices for writing tests, and a look at popular and obscure tools to facilitate the task.

During the presentation, we looked at many tools: NUnit, TestDriven.NET, Resharper's test runner, TypeMock, Selenium, Ivonna, NUnit's RowTest TestCase (I still think 'RowTest' is more descriptive than 'TestCase', and I'm still annoyed I had to update all my code when I switched to NUnit 2.5. Find/Replace is a wonderful tool though.)

One of the coolest things is that the goal of writing tests is so you spend less time debugging. Quite honestly, I'd rather write code than debug code any day. The key though is knowing what to test. Testing the .net framework or your database connection string is probably not the wisest use of your time. Testing that your business logic works the way you think it does is definitely a good way to keep bugs out of your code. To the depth that you can make the testing process seamless in your development, it can become an invaluable tool. Testing is an up-front investment that yields a long-tail benefit in improved code quality. If you can afford the investment, it's well worth it.

One of the coolest points that came out during the discussion was using your suite of tests as documentation, new developer training, etc. If your tests are run frequently and encompass the bulk of your code, that's where the best definitions of your business logic reside. What an awesome idea.

The slides and code are available here. If you're going to use the TypeMock and Ivonna content, you'll need to download trial licenses from here. All the other tools we discussed are open-source or free.

** NOTE: For some strange reason, my server wants to gzip zip files when it downloads them, leading to a double-compressed file. If you're downloading the zip files in IE, it'll tell you the file is corrupt. The fault is totally mine, but the file is not corrupt. If you download the file with FireFox, it will come down just fine. If you're an IIS guru and know how to disable gzipping zipped content while enabling gzipping for non-zipped content, please let me know. **

Desert Code Camp Sessions

Yesterday we had an awesome time at Desert Code Camp learning from each other and sharing ideas with each other. I got to present 3 sessions during the day, and here are the slides to each:
** NOTE: For some strange reason, my server wants to gzip zip files when it downloads them, leading to a double-compressed file. If you're downloading the zip files in IE, it'll tell you the file is corrupt. The fault is totally mine, but the file is not corrupt. If you download the file with FireFox, it will come down just fine. Since you'll need FireFox to use FireBug, you'll be just fine. **

Thinking in JavaScript: This is an awesome introduction to what makes JavaScript different from other C-Style languages such as C#, Java, C/C++, Perl, etc. There are a few differences you need to understand, and with those in mind, you can leverage your existing skills in this new world with ease.

Intro to jQuery: In this session, we look at the most common use of jQuery -- picking something, then doing something with each item selected. For example: $("p").css("background-color","#cccccc"); In that single line, we're able to search through the entire dom for all <p> tags, and set their background-color to gray. 1 line. That's awesome. We also briefly summarize some of the other stuff you get for your 26k now only 19k library script inclusion: AJAX, Animation, Traversing and modifying the dom, etc.

jQuery Deep Dive: Here we briefly reviewed the "pick something, then do something with each" techniques. Then we dive right in with AJAX: using GET and POST to retrieve content, sending parameters in the query string or as post parameters, techniques to call ASP.NET AJAX web services and page methods -- without a ScriptManager on the page, built-in jQuery animation, how use and build plugins, and how to interact with server-side controls and page life cycle. By the time we were done with all that, we were definitely ready for lunch.

Desert Code Camp is a great opportunity for coders of all skill levels and all technology backgrounds to share and learn.  I thank you for the opportunity to learn with you.  See you at the next Desert Code Camp.

"Command Prompt" in Windows Explorer Context Menu

There's a great power-toy for adding "Command Prompt" to the Windows Explorer context menu in XP here (or search for XP Power Toys). In Vista and Server 2008, you just hold shift when you right click to get it -- built in. But what if you're on Windows Server 2003 or another version? Well, the technique to add it is just a simple registry change:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\Command_Prompt]
@="Command Prompt"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\Command_Prompt\command]
@="cmd.exe /k pushd \"%1\""

Copy the above into a reg file (such as "Command Prompt Here.reg" -- no quotes) or similar, double-click to merge it, and you've got a Command Prompt selection in Windows Explorer's context menu. Easy as pie. If you want to "uninstall" it, open up RegEdit, navigate to that key, and delete it. Changes take effect imediately.

Note: some have reported this syntax brings up an error. If cmd.exe isn't in the PATH, this will utterly fail. In some cases I've had success with changing the command to be @="cmd.exe \"%1\"". In some cases, that utterly fails too. Your mileage may vary, batteries not included, don't use this product while sleeping on a curling iron or driving in traffic.

And to take it a step further, I modified the command name to "Command Prompt x86" and the command to "C:\Windows\SysWoW64\cmd.exe \"%1\"" and imported into a new key name, and I now have a 32-bit command prompt in the pop-up too.

(Now hopefully I won't forget it again.)

jQuery demo at SEVDNUG

It was a blast to present deeper concepts of jQuery at the SEVDNUG this evening. We enjoyed discussing ajax in jQuery, calling web services and page methods, building and using plugins, animation, and general best practices for JavaScript. The content from the demo can be found here. Note that because of a server misconfiguration, the file will come down double-zipped. Firefox seems to download the file just fine, while IE seems to choke every time.

For quick reference as well, here's the contents of the Resources page:

Tools

Learning and Reference Material

jQuery Plugins

FAQ