by Geoff Taylor
OpinionatedGeek Logo
Windows Tools

Source code for all these tools is on GitHub.

ADO.NET ConnTest A simple, free Windows program to test ADO.NET connection strings.

Lines of C# Ever wanted to know how many lines of C# code are in a file or folder hierarchy?  This free Windows program will tell you.

XmlTools Free tools to process XML files from the command line.

A long time ago (well, a couple of years ago) I wrote a wee app for the newly launched Windows Phone. This was mostly to get a bit of experience using the Windows Phone development tools, silverlight, and the app registration process. I didn't expect it to make much money.

Just as well, since it didn't! (It never got any promotion and it suffered neglect, so I know this is more a reflection on me than Windows Phone.)

But now I'm faced with the question: Do I delete the app from the marketplace?

It's not bringing in much - there's maybe one purchase every week or two. On the other hand, that's still 'free money', since it's happening without me having to do anything.

There aren't really any support requests for it, usually (although I did get one this week). Support would be annoying, since I don't even have the Windows Phone dev kit installed any more.

Plus I don't really have a lot of confidence in Windows Phone's future. I had a Windows Phone for a while, but I gave up on it. Microsoft seemed determined to be 'another Apple' rather than trying to do things differently. The typography on Windows Phones was stunning, but that wasn't enough to make me want to keep it. Android seems much more in keeping with my world view (but it has problems of its own).

And finally there's the fact that with Windows Phone 8, the way of doing things in 7 is deprecated. I don't even know if 8 will run the 7 silverlight apps, but even if it does silverlight apparently has no long-term future on the phone.

So overall I'm thinking about just removing the app from the MS marketplace. What do you think?

Posted by 'geoff' on Tuesday, 14 August 2012. No comments.

Live Tile from the Gold Price appI haven't written about .NET here in a while (and probably won't again for another while), but I wanted to put some thoughts up here in case other folks are looking for some details about Live Tiles on the Windows Phone 7.

These are things I learned before and during developing my Gold Price app, which is now available on the Zune marketplace. As well as looking up the current gold price on Yahoo, it puts the price on a Live Tile so you don't even have to open the app to stay current with the price of gold. Feel free to buy it!

Live Tiles are currently one of the unique features of Windows Phones. I love it that my Gold Price app isn't even possible on an iPhone. iOS just doesn't have the notion of 'live' icons on the start screen, beyond a simple (1-100) count. If you're Apple, you can do some fancy icon stuff (like the calendar app, which changes the icon to reflect the day and month), but if you're just a regular iOS developer you're out of luck.

To the left is a screenshot of the ‘start’ screen (of the emulator – getting screenshots from the real phone is Much Too Hard, unlike in iOS) showing the Gold Price app’s Live Tile. The tile updates every hour, with no user intervention. Live Tiles try to keep themselves visibly up-to-date, updating when the screen is unlocked but not bothering when the screen remains locked. If the phone owner is asleep, the phone naturally doesn’t bother making any network calls.

Here are some things I wish I learned about Live tiles a little earlier:

  • You can't have an animated Live Tile, unless you're Microsoft. In some ways this is a shame - I dislike restrictions on what developers can do when the OS creator just gets to do whatever the hell they like (such as background processing on both WP7 and iOS). On the other hand, I don't really want the weird and wacky world of Geocities-like animated tiles on my phone either. If there was an option to turn off animated tiles, I'd probably have them turned off anyway, so on balance I won't be losing too much sleep over this one.
  • The Live Tile image is (generally) created and hosted on a web server somewhere, not via a callback into your app code. This means there's a dependency on network connectivity for updating the Live Tile.
  • This network dependency also means apps that use Live Tiles need to have a server infrastructure behind them, even if it is only a small one. The ongoing cost of a web server means I think we’ll see fewer free Live Tile apps than if there was a server-less way of updating Live Tiles.
  • Mike Ormond has a great outline of how to plug a ShellTileSchedule into your app.
  • Problems with Live Tile updates are hard to debug. Everything must be just right for it to succeed, but many things can go wrong and I haven't found any way to tell where the problem is.
  • Your server must send the Live Tile image within 15 seconds of the request from the phone. This could be a problem if you're dependent on a web service call to another web server (like mine, which currently goes to Yahoo to fetch the pricing data). Network problems, latency, slow servers and bad DNS may be outside your control, but they can all make your image-fetching time lower than you’d like.
  • The image itself must be small (less than 80kb in size) It surprised me that the PNG generated for my Live Tile was so big when it's a compressed (albeit lossless) format. I ended up going with JPEG - it looks a little more washed out than the PNG version, but I'm confident it'll have no size issues.
  • It surprised me even more that the exact same .NET 4.0 code generated a much larger image file on my server than on my development machine. I ended up putting some code in the image generator/handler to check the size of the generated image (just so I could return an 'error' image that is guaranteed to be small enough) to help track down problems with fetching Live Tiles.
  • From discussions with other folks, it looks like the LiveTileSchedule adds the Live Tile update to the schedule, or resets the schedule for that tile if it has stalled because of an error, but - importantly - it doesn't change the time the update is scheduled to run.
    For example, if your Live Tile is scheduled to update in 5 minutes, and you load your app and it calls its LiveTileSchedule code, your Live Tile is still scheduled to run in 5 minutes. (Otherwise someone who opened your app every half hour would constantly reset any hourly schedule and the Live Tile would never update.)
  • It is possible to have your app trigger an immediate update of the Live Tile although the code (and the mechanism itself) seems horribly contorted. Mark Monster has a great description of how this all works. You have to set up a Push Notification channel, then have your app send itself a Push Notification telling itself that the Live Tile has been updated. Again, there's a dependency on network connectivity for this to succeed, so even if your app has all the data to generate the new Live Tile itself, and you embed the image generation code in your app, you still need a network available to send a Push Notification from your app to your app using a Live Tile URL local to your app.

If you’ve any comments, or if I’ve got any of this wrong, or if you’ve got a great example of a Live Tile app, please let me know or comment below!

Posted by 'geoff' on Sunday, 06 February 2011. 9 comments.

I had a lot of notes in Chandler, but I’ve started using Evernote for that kind of thing so I wanted to move them all so I only had to look in one place.

Nice as they both are, getting data into and out of them is quite a pain. In the end I exported from Chandler to an ICS file (the only portable format it can export to, as far as I can tell).

Then I converted that to RDF using ical2Rdf. Then I patched up the XML by hand to include the last edit date for the task (the closest I could get to the creation date).

Then I wrote a program to add each ICS entry to Evernote using ENScript. It’s great that they provide that tool, but it fails silently if you give it an empty text file for the description field. (It fails not so silently if you give it a text file without the .txt extension, but as long as it gives an error I can deal with it.)

So, here’s the source code for the program. It’s a nasty hack of a program, but I only had to get one successful run out of it so I’m not overly bothered. It might help someone else in the same boat.

No, you can’t ask me for support for this. I’ve already spent too long at this and I never want to see it again. You’re on your own, take backups, and don’t come crying to me if this deletes anything or everything.

Good luck!


Code Snippet
  1. namespace EvernoteIcalImport
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Diagnostics;
  6.     using System.Linq;
  7.     using System.Xml.Linq;
  9.     class Program
  10.     {
  11.         private static readonly XNamespace IcalNamespace = "";
  13.         private static int _counter = 0;
  15.         static void Main ()
  16.         {
  17.             XDocument toImport = XDocument.Load (@"..\..\ToImport.xml");
  18.             var document = toImport.Document;
  19.             var events = document.Descendants (IcalNamespace + "Vevent");
  20.             ProcessItem (events);
  22.             var todo = document.Descendants (IcalNamespace + "Vtodo");
  23.             ProcessItem (todo);
  25.             Console.In.ReadLine ();
  27.             return;
  28.         }
  30.         private static void ProcessItem (IEnumerable<XElement> items)
  31.         {
  32.             foreach (var element in items)
  33.             {
  34.                 string summaryText = "";
  35.                 var summary = element.Element (IcalNamespace + "summary");
  36.                 if (summary != null)
  37.                 {
  38.                     summaryText = summary.Value;
  39.                 }
  41.                 string descriptionText = "";
  42.                 var description = element.Element (IcalNamespace + "description");
  43.                 if (description != null)
  44.                 {
  45.                     descriptionText = description.Value;
  46.                 }
  48.                 string statusText = "";
  49.                 var status = element.Element (IcalNamespace + "status");
  50.                 if (status != null)
  51.                 {
  52.                     statusText = status.Value;
  53.                 }
  55.                 var createdElement = element.Descendants (IcalNamespace + "date").First ();
  56.                 DateTimeOffset created = DateTimeOffset.Parse (createdElement.Value);
  58.                 Import (summaryText, descriptionText, created, statusText);
  59.             }
  60.         }
  62.         private static void Import (string title, string description, DateTimeOffset created, string tag)
  63.         {
  64.             string arguments = string.Format ("createNote /n \"Chandler Import\" /t \"{0}\" /c \"{1}\" /i \"{2}\"", tag, created.ToString ("yyyy/MM/dd hh:mm:ss"), title);
  65.             _counter++;
  66.             Console.Out.WriteLine ("Arguments[{0}]: {1}", _counter, arguments);
  68.             var startInfo = new ProcessStartInfo (@"C:\Program Files (x86)\Evernote\ENScript.exe", arguments)
  69.                                 {
  70.                                     UseShellExecute = false,
  71.                                     RedirectStandardInput = true,
  72.                                     RedirectStandardOutput = true,
  73.                                     RedirectStandardError = true,
  74.                                     CreateNoWindow = true
  75.                                 };
  77.             var process = Process.Start (startInfo);
  79.             if (string.IsNullOrEmpty (description))
  80.             {
  81.                 description = "Empty import";
  82.             }
  84.             process.StandardInput.Write (description);
  85.             process.StandardInput.Flush ();
  86.             process.StandardInput.Close ();
  88.             process.WaitForExit ();
  90.             string output = process.StandardOutput.ReadToEnd ();
  91.             Console.Out.WriteLine ("Output:");
  92.             Console.Out.WriteLine (output);
  94.             string error = process.StandardError.ReadToEnd ();
  95.             Console.Out.WriteLine ("Errors:");
  96.             Console.Out.WriteLine (error);
  97.             if (!string.IsNullOrEmpty (error))
  98.             {
  99.                 Console.In.ReadLine ();
  100.             }
  102.             return;
  103.         }
  104.     }
  105. }

Posted by 'geoff' on Wednesday, 30 December 2009. No comments.

Since I don't have a life, like you regular folks, this weekend I extended the Atom support in this here blog.  Now, before you switch off, this should allow me to use Atom-enabled tools like Windows Live Writer to create posts with embedded images and automatically upload them to the server as well as the post itself.

The images, as well as the post's text, are stored in my li'l database.  I'm not at all sure how well that'll perform, but we'll give it a go.

Posted by 'geoff' on Sunday, 20 April 2008. 1 comment.

There really is a dearth of documentation on IIS6's wildcard redirection.

It seemed simple - I was renaming the Blog.aspx page in my blog to BlogEntry.aspx (no, don't ask why) and I wanted all the requests for the old page to go to the new one.

Similarly, I wanted requests for the Rss2.aspx page to go to the Rss.aspx page.  (Don't ask about this either.)

Except IIS's wildcards don't work the way you expect, and the expected redirection rules led to either:

  1. No rewriting,
  2. Incorrect rewriting, or
  3. Really, really incorrect rewriting that kept rewriting until the URL was too long for it to deal with.

So, a frustrating half-hour later, after trying every interpretation of the two documents Microsoft provides I could think of, I've come up with a rewriting rule that works.

And I'm going to share it with you now.

Remember, this is a fairly trivial thing to redirect (confused a little by the way my blog puts querystring parameters in the path, I admit).  And yet this is the most complex example of IIS URL rewriting I've seen.  The examples in the documentation and the ones I've found elsewhere are much more trivial (something I didn't think would be possible).

Are you ready?  Here goes:


That does the trick, for me at least.  I really figured something like:


would have worked...

Posted by 'geoff' on Thursday, 03 April 2008. No comments.
I just saw a post on Novell's 'Cool Blogs' blog that has a useful tip on base64 decoding.  Apparently, if you save your base64 text to a file with an extension of '.b64', Winzip can open the file and allow you to extract the content.
That's pretty neat, if you ask me.  But since I don't have Winzip installed, I can't check it out for y'all.  (OK, I could install it, and I already have a valid license for it, but I've been running for months with just Windows ZIP handling and a bunch of PowerShell scripts for command-line ZIPping and I'm reluctant to change now.)
Speaking of PowerShell, I have a couple of PowerShell scripts for base64 encoding and decoding too, if anyone's interested.
Those PowerShell scripts can come in handy for folks that the web interface isn't quite right for.  I got another un-replyable email (here's a link to the last one) a few days ago, that said: 

I want to encode over 100 passwords so that I can use LDIFDE to change them. My question is how do I format my input file so that the results I obtain show 100 difference encoded entries.

And, just in case he's reading this (or in case it's something someone else wants to do), here's my reply:

Hi there,


If you believe in using the right tool for the job, I wouldn't use the base64 web page on my site...  It'll always just generate one output based on all the input, not separated out the way you need.


If I were you, I'd use PowerShell for this kind of task.  It's fairly straightforward to put together a simple PS script that generates the base64 value from text on the command line, then you can just use a PS command to read in the file and pipe line-by-line to your script.


For instance, here's a simple file:


PS> get-content test.txt

This is line 1

This is line 2

Some more text


I've got a script called encode-texttobase64 that takes one command-line parameter - the text to encode - and it base64 encodes it and outputs the result.  So here's running the script, getting one base64 encoded value per line:


PS> get-content test.txt | %{ encode-texttobase64 $_ }





It's pretty straightforward.  I can give you the PowerShell scripts for encoding and decoding from the command line, but I thought you might relish the opportunity to create them yourself first (although they're really trivial).


BTW, I've never used LDIFDE, so I don't know if this does exactly what you want or if there's some other hurdle you need to overcome.


Good luck!



Posted by 'geoff' on Saturday, 12 January 2008. 1 comment.
What do you do when you get an email asking you a question, but your reply with the answer bounces?
I'm not sure what the best answer is, but I figure if I put the answer up here it might help someone else with the same question, even if the original asker doesn't find it.
The email I got was about my online base64 encoder and decoder:


I ask you some help


I have big problem of base64 decode

please help me.


please base64 decode source in be given me

Now don't laugh - his English is a hell of a lot better than my Korean.
The thing is, base64 encoding and decoding in .NET is utterly trivial - as long as you know the base class library calls to make.  If you don't know they're there, you can spend a while implementing your own algorithm, and that's just Not Fun.
So, here's my reply.  Maybe it'll help someone else find the right functions to call.

Hi there,


I could send you the file, but I don't think it would help you much.  There's an awful lot of ASP.NET 'plumbing' code and very little base64 code.


Instead, here's a sample program I just wrote that shows how to do the base64 encoding and decoding in .NET.  It's in C# - scroll down for the VB.NET version:


using System; using System.Text;   public class Base64Decoder {       public static void Main ()       {             string inputText = "This is some text.";             Console.Out.WriteLine ("Input text: {0}", inputText);             byte [] bytesToEncode = Encoding.UTF8.GetBytes (inputText);               string encodedText = Convert.ToBase64String (bytesToEncode);             Console.Out.WriteLine ("Encoded text: {0}", encodedText);               byte [] decodedBytes = Convert.FromBase64String (encodedText);             string decodedText = Encoding.UTF8.GetString (decodedBytes);             Console.Out.WriteLine ("Decoded text: {0}", decodedText);               Console.Out.Write ("Press enter to finish.");             Console.In.ReadLine ();               return;       } }


Here's my attempt at translating that to VB.NET.  I'm not a VB.NET programmer, so I may have done something silly.  It seems to work though.


imports Microsoft.VisualBasic imports System Imports System.Text   public module Base64Decoder       sub Main             Dim inputText As String             inputText = "This is some text."             Console.Out.WriteLine ("Input text: {0}", inputText)               Dim bytesToEncode As Byte()             bytesToEncode = Encoding.UTF8.GetBytes (inputText)               Dim encodedText As String             encodedText = Convert.ToBase64String (bytesToEncode)             Console.Out.WriteLine ("Encoded text: {0}", encodedText)               Dim decodedBytes As byte()             decodedBytes = Convert.FromBase64String (encodedText)               Dim decodedText As String             decodedText = Encoding.UTF8.GetString (decodedBytes)             Console.Out.WriteLine ("Decoded text: {0}", decodedText)               Console.Out.Write ("Press enter to finish.")             Console.In.ReadLine ()       end sub end module 

If you don't want to mess around with byte arrays, you can just use Convert.ToBase64String and Convert.FromBase64String, but doing the conversion to/from a byte array gives you a bit more control if you're faced with strange character encodings.

Hope this helps,



Unfortunately that reply (to a Korean email address) bounced with the error "PERM_FAILURE: SMTP Error (state 13): 551 User not local". Maybe the person who originally asked the question will find the answer in the blog post.

Posted by 'geoff' on Thursday, 06 December 2007. 10 comments.
A friend is thinking about moving his blog from the free Wordpress hosted service.  I figured I'd try to persuade him to go to a .NET-based blog, and dasBlog seemed a good candidate.
The biggest problem so far has been moving the existing blog entries and comments.  Wordpress does have an export function, but it's not very good.  Instead of outputting something other blog engines can understand, Wordpress has its own 'extended' version of RSS.  Yuk.
The good news: There's a replacement 'export.php' file that exports BlogML!  Yay!
The bad news: Since his blog is hosted on Wordpress' own server, there's no way to put the replacement export.php file on his site to run it, to get the BlogML.  Boo!
So I wrote a program to grab the data from the 'Wordpress eXtended RSS' (you again) file, and use dasBlog's API to import it into the new blog.
Surprisingly enough, it worked.  (To be fair though, I just snarfed the code from Scott Hanselman and hacked it to work with Wordpress.)
So, here it is in case it ever proves useful to you.  It's not code I'm particularly proud of, but it worked the one and only time I needed to run it.  It might be a useful starting point for someone else.

using System;

using System.Xml;

using newtelligence.DasBlog.Runtime;


namespace OpinionatedGeek.Applications.WordPressToDasBlog


    internal class Program


        private const string ContentNamespace = "";

        private const string WordpressNamespace = "";


        private static int _entryIdCounter = 0;


        private static void Main ()


            IBlogDataService dataService = BlogDataServiceFactory.GetService (AppDomain.CurrentDomain.BaseDirectory + "\\content", null);


            XmlDocument exported = new XmlDocument ();

            exported.Load (@"..\..\wordpress.xml");

            XmlNamespaceManager namespaces = new XmlNamespaceManager (exported.NameTable);

            namespaces.AddNamespace ("content", ContentNamespace);

            namespaces.AddNamespace ("wp", WordpressNamespace);


            XmlNodeList items = exported.SelectNodes ("/rss/channel/item");

            foreach (XmlNode item in items)


                ImportBlogEntry (dataService, namespaces, item);



            Console.In.ReadLine ();





        private static void ImportBlogEntry (IBlogDataService dataService, XmlNamespaceManager namespaces, XmlNode item)


            DateTime postDate = DateTime.Parse (item ["pubDate"].InnerText);


            string blogText = item ["content:encoded"].InnerText;

            string blogTitle = item ["title"].InnerText;

            string guid = item ["guid"].InnerText;

            Entry entry = new Entry ();

            entry.CreatedLocalTime = postDate;

            entry.ModifiedLocalTime = postDate;

            entry.Title = blogTitle;

            entry.Content = blogText.Replace ("\r\n", "


            // There seems to be a problem with dasBlog's entry lookup code.  It HTML encodes the

            // entry ID to do the lookup, but they're stored unencoded (as far as I can tell, which

            // isn't very far).  So, we need to use an entry ID which is the same when HTML encoded

            // and unencoded.  This seems to rule out the normal GUIDs that Wrodpress uses (which

            // are just entry URLs).  Let's keep it simple and use an increasing int counter.

            //entry.EntryId = guid;

            entry.EntryId = (++_entryIdCounter).ToString ();

            string categories = "";

            foreach (XmlNode categoryItem in item.SelectNodes ("category"))


                categories += categoryItem.InnerText + ";";


            categories = categories.Trim (';');

            entry.Categories = categories;

            entry.Author = "Paul";

            entry.AllowComments = true;

            dataService.SaveEntry (entry);


            Console.Out.WriteLine ("Title: {0}", blogTitle);

            Console.Out.WriteLine ("Date: {0}", postDate);

            Console.Out.WriteLine ("Categories: {0}", categories);

            Console.Out.WriteLine ("GUID: {0}", guid);


            foreach (XmlNode commentNode in item.SelectNodes ("wp:comment", namespaces))


                ImportComment (entry, dataService, commentNode);






        private static void ImportComment (Entry entry, IBlogDataService dataService, XmlNode commentNode)


            DateTime commentDate = DateTime.Parse (commentNode ["wp:comment_date"].InnerText);

            string commentText = commentNode ["wp:comment_content"].InnerText;

            string commentAuthorName = commentNode ["wp:comment_author"].InnerText;

            string commentAuthorEmail = commentNode ["wp:comment_author_email"].InnerText;

            string commentAuthorHomepage = commentNode ["wp:comment_author_url"].InnerText;

            string commentAuthorIPAddress = commentNode ["wp:comment_author_IP"].InnerText;


            Comment comment = new Comment ();

            comment.CreatedLocalTime = commentDate;

            comment.ModifiedLocalTime = commentDate;

            comment.TargetEntryId = entry.EntryId;

            comment.TargetTitle = entry.Title;

            comment.Author = commentAuthorName;

            comment.AuthorEmail = commentAuthorEmail;

            comment.AuthorHomepage = commentAuthorHomepage;

            comment.AuthorIPAddress = commentAuthorIPAddress;

            comment.Content = commentText;


            Console.Out.WriteLine ("Comment Author: {0} ({1})", commentAuthorName, commentAuthorEmail);

            Console.Out.WriteLine ("Comment IP/Home Page: {0} ({1})", commentAuthorIPAddress, commentAuthorHomepage);

            Console.Out.WriteLine ("Comment Date: {0}", commentDate);

            Console.Out.WriteLine ("Comment: {0}", commentText);


            dataService.AddComment (comment);






I can't help thinking it would make a neat PowerShell command...
Anyway, hope it's useful, and if it breaks things you get to keep both pieces.

Posted by 'geoff' on Friday, 19 October 2007. 1 comment.
If you're here because you've just done a Google search for an error like "The VirtualPathProvider returned a VirtualFile object with VirtualPath set to '/Page.aspx' instead of the expected '//Page.aspx'" then I have some good news for you.

The reason you get the error is that your virtual page has a compilation error in it.  It has nothing to do with your VirtualPathProvider, really.

What seems to be happening is that it calls your VirtualPathProvider for your page, gets the appropriate Stream that points to the file's contents, tries to compile the page, fails, and then calls your GetFile () with the same parameter but with an extra '/' at the front.

I have no idea why it does this.

I'm also sad that Google let me down (there was only one hit and it didn't have an answer) and I had to figure this out for myself.  I mean, having to actually think?

And I'm embarrassed at how long it took me to figure out...  Still, hopefully it'll help someone else out.

Posted by 'geoff' on Thursday, 17 May 2007. 4 comments.

Remember I said yesterday how I hadn't managed to catch up with beta 1 of Microsoft's AJAX ASP.NET? Well, to really rub it in, they've released beta 2 now.


Posted by 'geoff' on Thursday, 09 November 2006. No comments.
RSS 2.0 Subscribe to the RSS 2.0 feed for Geoff’s Blog.