Refreshing Reports using URL access in Reporting Services
In the past I've been a fairly big advocate of the Microsoft Report Viewer control for accessing SQL Server Reporting Services reports from within a web application, simply because it allows authentication to be proxied through the host application (so you can use a service account to talk to reporting services and control security through your application rather than adding every user in your application to reporting services), but the control requires a traditional ASP.Net webforms Form and page life cycle, so it's out of the picture for ASP.Net MVC development.
To solve the problem I turned to Reporting Services' URL Access functionality that allows direct linking to reports while still giving you control over parameters, the toolbars and other functionality through the use of variables passed in via the query string. I used iframes to get the inline reports onto the page, and just linked directly to the reports that were meant to open in their own window.
This was great until I noticed that the data on the report was not being refreshed at a regular interval, leaving users with stale data on screen. A quick check of the report in Report Manager confirmed that the data was in the system and the report updated correctly when shown through Reporting Services' own interface.
A quick inspection of the execution parameters of my report (located under properties->execution for each report in Report Manager) showed that I was not using the in built Reporting Services caching, so I figured I was dealing with some sort of HTTP caching. This lead me to try one of the oldest tricks in the "how to fool the HTTP cache duration" book, random junk parameters. The theory behind this is that adding a random parameter that is not used by an application to the end of a query string will tell the web server that this is a request that it hasn't seen before, and hence it will need to figure out the actual result rather than looking in the cache. I postfixed the URL with something along the lines of "&RefreshParameter=" + System.DateTime.Now.Ticks.ToString(), and hoped for the best.
Unfortunately it did nothing. Realising that the problem had nothing to do with HTTP caching I thought that I'd try and apply the same trick to an actual report parameter to see if reporting services could be fooled in a similar manner. The URL Access feature allows you to specify reporting services parameters on the query string in the exact same format as the "RefreshParameter" I had already created, so I simply had to make a new parameter on each of my reports called "RefreshParameter" that accepts blank/null values and does nothing. This worked a treat, enabling me to have current data displayed in iframes on my MVC site.
Update: Peter Van Ooijen suggests that using the rc:ClearSession=true parameter may work in some scenarios (but apparently not all). Some people seem to have had good mileage by combining both the session clearing and the random variables.
Now You See It
Today I was helping out one of my colleagues with a particularly annoying InfoPath problem; he wanted to enable the multi-line option for a text box bound to a web service, but whenever he did the read-only option would automatically trigger. Truly frustrating.

Not being one to believe that things are impossible, I decided to take a crack at it at home, and as soon as the kids were in bed I got stuck into Everyone's Favorite Search Engine (TM). Unfortunately, Google was not my friend on this problem; I found all sorts of information about binding Rich Text Boxes to Web Services and forcing text boxes to re-size when using Forms Server, but nothing related to editable mutli-line text boxes.
I decided that the best idea was to try and replicate the problem, so I fired up my trusty Win2k8 VM, made a quick ASMX web service and hooked it up to an InfoPath form. Sure enough, as soon as I tried to enable the Multi-line option of a text box it became read only. Reassuringly, it worked when using an unbound text box, which at least let me know that displaying editable mutli-line text boxes in InfoPath is possible.
The fact that the problem was only occurring with web service data-sources made me suspect that the schema being returned by the web service may be the key, but upon inspection the schema InfoPath created for my web service connection was using exactly the same types as the schema for my default form data-source. The elements were not read only, and I couldn't see anything else out of the ordinary in the schema. So much for that idea.
Then it clicked. The problem had been staring me in the face the whole time in the guise of a huge red circle with a white "x" through the middle; the form was configured for InfoPath Form Services compatibility, which disables certain options. This MUST have something to do with the problem. I quickly turned off Form Services compatibility then turn on the Multi-line option again, and....
... the field became read only. Doh! But wait! There were two sub-options under Mutli-line: Paragraph breaks and Wrap text. In Form Services compatibility mode I could not edit these options, and selecting Mutli-line highlighted both, which in turn highlighted the read-only option, but with compatibility mode turned off I was now free to edit these values.
Then it hit me. I noticed that, by default, I couldn't select the Paragraph breaks option unless the read-only option was checked. That was it! For some reason, InfoPath can't handle having paragraph breaks in a text box when it's bound to a web service
So instead of clicking on Multi-line I clicked on Wrap text. This checked the Mutli-line option for me, and at the very least let me have a text box that could wrap text onto a new line. I could live with not being able to have paragraph breaks, but unfortunately I could only select this mode in InfoPath Form Services compatibility mode, meaning that I wouldn't get all the fancy error checking to make sure that it would work when viewed over the web. When in compatibility mode, clicking on Multi-line would still check the Paragraph breaks option which would then force the control to be read only. I couldn't win.
Or could I? On a whim I turned off compatibility mode again and set up my control to be Mutli-line/Wrap text as described above, then I turned compatibility mode back on. Success! The options stayed as they were, giving me a mutli-line text box that wasn't read-only. I would just have to make the necessary multi-line related changes to my fields with compatibility mode turned off, and only turne it on for the specific occasions where I would need it.
Not a great solution, sure, but at least it works, and that's certainly better than nothing. I'm honestly still amazed that I couldn't find any info about this on Google, though. I would have thought that using a bound multi-line text box would be a more common use case.
The Future is Looking Good
As I've previously mentioned, as of October 1st 2008 MSDN subscribers who have previously only had access to Visual Studio Team System Development Edition can now also use Database Edition. The reasoning behind this move has now been made clear thanks to the January MSDN Subscription Newsletter:
In the next release of Visual Studio Team System we will be merging the feature sets of the Development Edition and the Database Edition into a single product. The new product will include all of the features in the Development Edition and the Database Edition as well as new capabilities delivering even more value in a single product. This will provide a more complete set of capabilities for building software in today’s data-driven environments. Bringing these two feature sets together enables you to take advantage of the core tools for application development as well as the necessary tools for database development, including performance profiling, code analysis, code metrics, code coverage, database refactoring, Schema Compare, Data Compare, and more
Looks like they've finally got the message that a large proportion of developers still need to touch the database, and as a result will be combining the Developer and Database editions of VSTS in the next version. Now if only they could get rid of the comparatively useless Architect edition. Roll on VS 2010!
The Misleading Error Message Strikes Again
We've got a stock standard Windows 2008 virtual hard drive image at work that I wanted to make some additions to and generally polish up into a decent developer image. The install media was the "Windows Server 2008 Datacenter, Enterprise and Standard (x86) - DVD (English)" ISO sourced from MSDN.
I noticed that the image had not yet been activated, so I set out to rectify the situation. Hopping into the system settings in control panel, I navigated down the the section on product key information and clicked on the link to activate windows. Unfortunately this process errored out with the following message:
Code: 0x8007232B
Description: DNS name does not exist.
I immediately checked network connectivity. Can I get to google? Yes. Can I resolve MS addresses? Yes. Hmm.
Then I happened to notice some text in the bottom right hand corner of my desktop:
Windows Server(R) 2008 Standard
Evaluation copy. Build 6002
Evaluation copy? That means that my product key is not going to be valid. So I grabbed a new product key from MSDN, entered that in and tried the activation process again. Success!
After all that I still can't figure out what a DNS error has to do with a product key being invalid. I would have been more than happy with something along the lines of "Unable to activate due to invalid product key". They may as well have told me that activation failed due to insufficient vegemite.
InfoPath Form Security and Custom Code
Generic error messages are high on my list of pet peeves. Being told that "an error occurred" may be something that you can pass off in a line of business application, especially if the actual error message gets emailed to the help desk or something similar, but it is totally unacceptable in a product targeted towards software developers.
Tonight, when trying to add some managed code to an InfoPath 2007 solution, I ran into one of these horrible errors. InfoPath was presenting me with a highly helpful "InfoPath cannot open the selected form" error message whenever I tried to debugging the solution.
After spending a few minutes scratching my head I remembered that InfoPath forms need to run in Full Trust mode in order to use managed code. I navigated to "Tools>Form Options>Security and Trust" and had a look at the settings, which were still set to the default "Automatically determine security level". Unfortunately I assumed that, when using this setting, InfoPath would be able to figure out that I was using managed code and select the appropriate security level. Obviously my assumption was incorrect.
After setting the level of trust to "Full Trust" I was finally able to launch the form, complete with managed code.
If I had not previously read about security and trust within InfoPath then I would have had to waste more time floundering on Google with only a generic error message to help me. All that could have been avoided if they had just presented me with a dialog saying "The form has an insufficient trust level to perform this operation". Well, either that or make the "Automatically determine security level" trust setting actually determine an appropriate trust level in the first place.
DataDude for All
Picking an appropriate version of Visual Studio Team System for your team to use has always been an interesting proposition. The full Team Suite edition is outside the price range of most projects, and you have to make sure that the majority of your team will be able to see all of the artifacts with minimal trouble. Using features such as the infrastructure diagramming tools or database projects can often mean that you have one machine capable of performing that function, which can act as an annoying bottleneck for your team.
The choice of product becomes an even tougher question if you are a Microsoft Gold Partner (with ISV or CDS competency), as Gold Partners have traditionally gotten a certain number of VSTS Developer Edition licenses as part of their partner pack. This is great, but it makes it really hard to justify the cost of another version of VSTS. Other versions are now $4000 dollars more expensive than Developer edition, rather than all of the editions being the same price.
I have always maintained that, bang for buck, VSTS Database Edition is the best value for money out of all the VSTS editions, and, given a choice, I would pick this for every member of the team so that they could all use the database source control feature. It's the feature I always miss whenever I don't have the full Team Suite edition of Visual Studio installed.
There was much rejoicing, then, when I found this lovely little tidbit on the MS Partner site.
Certified Partners who have earned either ISV and/or CDS Competency are upgraded to Microsoft Visual Studio Team System 2008 Development Edition with MSDN Premium Subscription. Starting 1 October, 2008, these Certified Partners will be able to download VSTS Database Edition from MSDN Subscriber Downloads as part of their upgrade to VSTS Development Edition with MSDN Premium Subscription.
It seems that we now get the choice between VSTS Developer Edition and VSTS Database Edition. I for one am extremely happy with this news. Test Edition and Architect Edition have some nice features (most notably the performance testing tools in Test Edition which run rings around the old Application Centre Test product that came with 2003), but for me these two products cover the majority of the Team Suite features that most development teams would need.
Expand Your Mind
After the recent release of Visual Studio 2008 SP1 and .Net 3.5 SP1 I thought it would be a good idea to upgrade my Windows development environment to take advantage of some of the cool new features. I do .Net development on my Ubuntu machine by using VirtualBox to run a Windows 2008 Virtual Machine. It works quite well, especially if you're mainly doing server side stuff or Windows Forms. I'm not sure how it would handle the DirectX goodness of WPF, but I'm willing to live without that for the moment, and I can always install Vista x64 on another box if need be.
When I originally set up the VM, 20 gig sounded like a good size for the hard drive, so I set up a 20 gig dynamically expanding VDI file using VirtualBox and started installing. The dynamically expanding disks are cool in that they will only expand if need be, so if you create a 300 gig image and only use 20 gig of it, the VDI file for the drive only uses 20 gig of your actual disk space disk. I wasn't planning on installing office or anything else, so I thought that the drive would be more than big enough. How wrong I was.
After installing the Windows 2008, Visual Studio 2008, SQL Server 2008 and Firefox 3.0 I had around 3 gig free. Where did all my drive space go??? "No matter", I thought, "3 gig should be enough to work with for the moment, especially as I'm using network drives for all my data storage". I continued working like this for a few months, right up until I went to install the VS 2008/.Net 3.5 Service Pack 1 updates.
Half way through the install I merrily discovered that the service packs require over 6 gig of free space just to install themselves. But.. but.. but... I don't HAVE 6 gig of free space! It should be noted that the installation program says that it is merely "recommending" that you have this amount of free space, but it won't let you continue unless you free up the "recommended" 6 gig. That's a pretty definite recommendation.
My first thought was that to free up that amount of space I would have to uninstall either Visual Studio or SQL Server, which would sort of defeat the purpose of installing the service packs. Obviously this wouldn't do, so I went looking for ways to save a few gig here and there, and quickly discovered that I had a 2 gig page file by default. I reduced this to 200 meg or so, but I still needed to free up more space to install the service packs.
At this point I gave up on the idea of trimming back my install to allow the service pack to continue. I went hunting for some free hard drive cloning software, and found the excellent (and free) HDClone. HDClone allows you to copy one hard drive to another, and it can also expand partition so that it takes up all the space on the new drive.
I downloaded the free version of HDClone and mounted the ISO in VirtualBox, then I created a new 230 gig dynamically expanding VDI file which should be enough to install the service pack (cough). I mounted the new drive as the secondary master for my Windows 2008 Virtual Machine and mounted the ISO in the virtual CD-ROM drive.
A quick reboot of the machine presented me with the HDClone interface, and in 4 clicks I was ready to copy my drive. It even asked me if I wanted to covert the partition to take up all 230 gig of the new virtual drive. I let it do the conversion and left it to copy all my data.
An hour or so later it was done. HDClone asked me if it wanted me to automagically set up my partition data, so I let it do that and then reboot. "Yay!", I thought, "finally I'll be able to install the service pack!".
Unfortunately it was not to be. When trying to boot off my new VDI file I discovered that it wouldn't boot, even though it new the OS was on there. I dug around for my Win2k8 install media and went into repair mode. Sure enough, Windows was reporting my partition as being 0 meg. Something wasn't quite right here.
I could have downloaded something like the Ultimate Boot CD to play around with the partitions, but it was late and quite frankly I couldn't be bothered, so I booted back into HDClone again and redid the copy, this time making sure that I said "no" to its overzealous partition magic (no pun intended).
Once that was finished I removed the old 20 gig VDI, gave it a reboot and bingo, I now had Win2k8 installed on a 224 gig drive. This was more than enough space for the Service Pack 1 installer to work with. I might even have enough room to install MOSS 2007 now.
Get Back
I've been using the Firefox 3.0 beta since I upgraded to Ubuntu 8.04, as it came as a standard package, but until now I had no idea that version 3.0 had a significantly different UI from 2.0. In fact, I only found out when I saw recent Flickr posts showing a Firefox comparison chart with a list of "advantageous" features. One in particular stuck out:
Large "back" button, the result of decades of market research
Large back button? I couldn't see a large back button on my Ubuntu install, even though I was running the latest 3.0 release. What were they talking about?
After installing Firefox 3.0 on my Windows 2003 machine at work I soon found out: They've enlarged the back button, presumably because it's the most commonly hit icon in the tool bar, and it looks pretty cool. I must say that I don't use the back button much, I'm more of a "delete key == back" kinda guy, but it's a nice touch.
Apparently there's a bit of backlash against this new feature, but at least they're trying. It's certainly less jarring than IE7's "hide the menu" approach to UI innovation, and it makes quite a lot of sense from a practical point of view.
I've now upgraded my Mac and XP machines to Firefox 3.0, and I'm enjoying the new UI. As an aside, and regardless of John Gruber's list of gripes, I still think that Firefox 3.0 on the Mac is a huge step forward in comparison to what they had. Versions 2.0 and 3.0 are like chalk and cheese. But I digress..
So why didn't my Linux install have this spiffy new button? Well, it turns out that Firefox 3.0 on Linux is one of the latest applications to get the Tango treatment. Tango is an attempt to bring some order to the world of open source applications by giving them a sense of consistency. This is actually a great idea, and it's done wonders for applications such as OpenOffice.Org and The GIMP, which now look somewhat respectable. There's a showroom full of projects that have had a Tango related makeover, and most of the featured applications are much better off with the new icon set.
This is all well and good, but what about Firefox? It's probably the biggest open source project in next to Linux itself, and yet it's not yet mentioned in the Tango Showroom. Surely there's a reason for this?
Well, a potential reason becomes fairly obvious once you actually open up the browser. The very first page that pops up has a picture of what the new theme looks like on Windows Vista, which is a stark contrast to what you are seeing in your native browser window.
See what I mean? The "standard icon" idea that has worked so well for OpenOffice.Org, The GIMP and VMWare Workstation has given us a fairly plain browser that has lost all traces of the innovation that the Firefox team were trying to implement. It looks pedestrian and boring, in fact if it wasn't for the deli.cio.us plugin icons I think I'd never get any work done, as a mere glance at my Firefox toolbar would send me straight to sleep.
It's sad that the most interesting, and therefore the one most likely to draw your attention at a casual glance, icon in the standard set now being the home icon. Although it is much more visually appealing than the plain forward and back arrows, it's a button that I can't remember ever clicking. Not even once. There are no paint brushes or printers to mix things up and keep your eye from settling on a useless feature that you've never wanted and will never need.
The great thing about Firefox is that you can apply a theme to get it to look however you want, so you would think that I would have no need to complain. I could just pick a theme from the Firefox website and would be happy, right? Well, yes and no. There's no "official" theme from Firefox that delivers similar functionality for Linux. There might very well be a 3rd party theme that will make my browser act like this (I did look for one without success, but that's another story altogether), but that still doesn't change the fact that the Firefox team have taken a vastly different approach to their Linux UI when compared to OSX and Windows, one which flies in the face of their obvious urge to innovate.
Alex Faaborg spoke about how they wanted to integrate with the host OS as much as possible on the proprietary platforms, and that didn't stop them from making the forward and back buttons look absolutely nothing like any native control on XP, Vista or OSX, so their integration manifesto couldn't have been that rigid. With Linux they have gone for total homogonisation, where as OSX and Windows have an "integrated, but with individual flair" feel. They haven't had all the life sucked out of them by a desire to conform to the nth degree. Standards are great, and are certainly needed on Linux given the diverse nature of the different window managers, but they shouldn't be used to stifle ideas and discourage creativity.
Twittering Heights
Until recently I honestly didn't "get" the Twitter phenomenon. I figured that it would be chock full of people posting about what they had for lunch, or other such inane comments.
There are quite a few excellent articles that go some way to refute this by pointing out the power of choosing who to follow, but that still wasn't enough for me. Finding people to follow seemed like a lot of hard work, and constantly maintaining this list was not of any particular appeal to me. I'd given up on trying to maintain a LiveJournal friends list, which also had the tendency to suffer from the same sort of noise problems, so why would Twitter be any different?
Thinking back, the reason I lost interest in LiveJournal had nothing to do with the work involved in tending the garden that was my friends list. I simply never felt compelled to post anything on the site, so I had no buy in. I was merely an observer, and I was never going to become interested in a service that I had no urge to contribute to.
Twitter is a little different. The short, 140 character messages and public API seemed like the perfect way of complimenting my blog, especially if I could put a Twitter feed on my site. I could augment the larger posts with short, train of thought ideas, and generally ignore the horrors of trying to accumulate another Facebook-like friends list and just concentrate on producing content. At last, a social networking site with a chance of holding my interest!
There are a few ways of getting at a particular Twitter feed. Aside from the obvious "code your own" approach, Twitter themselves publish a series of "badges" that provide a variety of Flash of Javascript driven ways of getting your Twitter info onto your site. This blog runs on WordPress, so I could also the variety of Twitter based tools for WordPress that let you create and display tweets on your blog. I ended up using the main Javascript driven "badge" provided by Twitter. All it took was some minimal styling and I had a working feed of all my tweets.
It only took a few hours for me to figure out that the side bar's performance was greatly affected depending on Twitter's now legendary availability problems, and the Twitter results wouldn't even show if you were behind a proxy server that didn't allow social networking sites. The feed implementation I chose worked by including two Javascript blocks sourced directly from Twitter, one being a file that defines two functions used to display tweets and the other being a JSON file representing a call to one of these functions with data from my feed. Obviously if a user was trying to view my site from a location that does not allow Twitter, e.g. most work places, then they would not see the feed on my site. Even if they could view Twitter, there was a good chance that the service itself would be down, causing an HTTP timeout to occur on the client.
One option would have been to move the Twitter call to my server, making a PHP call from the WordPress site to populate the list of tweets, or even pointing the existing Javascript at a script on my site. Contacting Twitter on the server would allow me to bypass the "social network" proxy tax, and it would also give me a point of contact to introduce a cache for the Twitter feed.
By caching the data on my server, and only fetching it from Twitter if the cache was older than a certain period of time, I could at least make sure that the feed would always display. If Twitter was down then I'd just have to wait for the request to time out and return the cache results regardless of age. Unfortunately any potential HTTP timeout could still affect the timeliness of the response from my blog, meaning that a user still had the potential to feel the wrath of Twitter's downtime on my site.
It seemed that, no matter what I did, contacting Twitter to retrieve my tweets was going to be an expensive and unreliable operation, and not one that I wanted to impose on the user when they request pages from my site. After all, I'm trying to augment my blog, not cripple it.
In situations such as this I'd normally install a service of some sort to do the request outside of the request/response life cycle experienced by users of the site. However, this site runs on shared hosting Linux environment with no SSH/shell access, and my only way to configure it is through FTP or a web interface, so I assumed that my options here would be limited.
Fortunately, after a bit of poking around in the web configuration tool, I discovered that my hosting provider lets me schedule cron jobs. This was the missing link that would allow me to use a cached approach and keep the same Javascript display code, while still enabling the cache to be updated outside of the normal request/response lifecycle. Effectively I could create a poor man's Twitter update service just by executing a Python script every 15 minutes.
In the interests of spending as little time as possible on the problem, I decided that I was going to keep all the existing Javascript for displaying the tweets. All I needed to do was point both the function definition and the JSON file links at copies on my site, and update the JSON file on a regular basis using a script executed via a cron job. A few lines of Python and some site configuration later and I had a Twitter feed that updates every 15 minutes.
One of the most interesting aspects of the exercise was deciding how to handle failures in the script. Twitter is down so often that I really don't care if the web request fails. So much so that I didn't even bother logging it. I've effectively assumed that the service will fail a good proportion of the time, and if it fails because Twitter couldn't be contacted then so be it. I get told about everything else, such as being unable to write to the cache, but I could really do without 50 email a day reminding me about the precarious nature of a service I have no control over.
The Saga Continues
After the success of her first programming lesson, my 5 year old daughter was eager to start the next one.
Previously we had covered boolean logic and control flow, both of which she had no trouble with, so she was ready for something slightly more complex. I had a few ideas about what to talk about next, but I didn't want to decide until we were both sitting at the desk, ready to start the lesson.
The first lesson really drove home the similarities between teaching adults and children. It's all about being able to capture someone's interest for long enough to get your point across. The good thing about kids is that they make it fairly obvious when their attention span is waning and they are no longer taking on information. Adults have a nasty habit of trying to be polite, which only ends up wasting your time and theirs.
I left the decision to the last minute so that I could take my daughter's current demeanor, energy levels and alertness into account. If she was easily distracted then I'd want to avoid some of the more complex topics, and only explain short, easy to swallow chunks of information.
This turned out to be a good call. She had previously discovered TuxPaint on my Ubuntu box, and was already asking if we'd have time to play "the penguin game" (i.e. TuxPaint). I immediately knew my chances of having her undivided attention were shot.
As an aside, I highly recommend TuxPaint. I've installed it on her laptop, an old G4 iBook we had lying around, and she plays with it most nights. TuxPaint also has the honor of being the first open source project I've ever donated to. The guys who make it even offer really cool kid's shirts so your pre-schooler can show their support for open source software. It almost makes up for the fact that they don't sell FireFox shirts in kid's sizes. Almost.
I ended up ditching any ideas of discussing encapsulation for something much simpler. I figured that I could sell the concepts of variables and functions before she got bored and started asking about that silly penguin again. These two concepts would be enough to round out her understanding of some basic procedural programs, and probably allow us to actually create something cool.
Variables were easy to explain. I was considering starting with a brief overview of lambda calculus, but that might get us a call from child services, so i toned it down a little. At the heart of it, a variable can be explained away as an alias for something. It's just another way of referring to a particular "thing".
I appealed to her natural sense of vanity, something every 5 year old has more than enough of, and explained variables in terms of her age. She loves writing her name, so this was a good attention-getter.
EMMA'S AGE = 5 # I used this to explain what happens on her birthday EMMA'S AGE = EMMA'S AGE + 1 EMMA'S AGE IS NOW 6
I explained that, after stating that her age was 5, we didn't need to say "5" over and over again. It was enough to say "Emma's age", because we knew that she was 5. I used her birthday as an example of changing the value of the variable. Every year she has to add 1 to her age, so on her next birthday she will be EMMA'S AGE + 1.
We tackled functions next. She loves baking, so I thought I'd use a concept of reusability that she would already be familiar with: a recipe. We wrote out a definition for baking a cake. Of course, I have no idea how to bake a cake, so I wouldn't recommend following the recipe...
BAKE CAKE()
{
Add Flour
Add Milk
Add Eggs
Add Sugar
Add Butter
Mix
Put in Oven
}
Now that we knew how to bake a cake, we could bake as many of them as we liked.
BAKE CAKE BAKE CAKE BAKE CAKE BAKE CAKE
After learning about functions and solving the world hunger crisis with cake we decided to call it a night.
Hopefully she's now learned enough to have a basic understanding of procedural programming, or at least enough to let her understand a simple programs, which is what we plan to do next. I don't expect her to be writing the next Crysis, but she should be able to sit over my shoulder and understand a Hello World or two.





