Blog      Products      DotNetWiki      Support      Contact  
     Blog Categories
 - All
 - .NET
 - 4 Word Book Reviews
 - AllPodcasts
 - Business Thoughts
 - Clueless Idiocy
 - Norn Iron
 - Personal
 - Podcasting
 - PowerPack
 - Weird Interweb Stuff
 
     Geoff on Twitter
 
     Local Blogs
 
  ASP.NET PowerPack
The ASP.NET PowerPack contains 28 rich, cross-browser controls including:
 - RichTextBox
 - ComboBox
 - DatePicker
 - No-Repost validator

Try the ASP.NET PowerPack free today!
 - More Info
 - Download
 - Price List
 - Licensing
 - Buy Now!

 
     Web Tools
 - The DotNetWiki
 - OPML Viewer
 - RSS Viewer
 - ASP.NET Colors
 - Base64 Encode
 - Base64 Decode
 - HTML Encode
 - HTML Decode
 - URL Encode
 - URL Decode
 - Crazy IPs
 - Whois

 
     Windows Tools

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.

 

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;
  8.  
  9.     class Program
  10.     {
  11.         private static readonly XNamespace IcalNamespace = "http://www.w3.org/2002/12/cal/ical#";
  12.  
  13.         private static int _counter = 0;
  14.  
  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);
  21.  
  22.             var todo = document.Descendants (IcalNamespace + "Vtodo");
  23.             ProcessItem (todo);
  24.  
  25.             Console.In.ReadLine ();
  26.  
  27.             return;
  28.         }
  29.  
  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.                 }
  40.  
  41.                 string descriptionText = "";
  42.                 var description = element.Element (IcalNamespace + "description");
  43.                 if (description != null)
  44.                 {
  45.                     descriptionText = description.Value;
  46.                 }
  47.  
  48.                 string statusText = "";
  49.                 var status = element.Element (IcalNamespace + "status");
  50.                 if (status != null)
  51.                 {
  52.                     statusText = status.Value;
  53.                 }
  54.  
  55.                 var createdElement = element.Descendants (IcalNamespace + "date").First ();
  56.                 DateTimeOffset created = DateTimeOffset.Parse (createdElement.Value);
  57.  
  58.                 Import (summaryText, descriptionText, created, statusText);
  59.             }
  60.         }
  61.  
  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);
  67.  
  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.                                 };
  76.  
  77.             var process = Process.Start (startInfo);
  78.  
  79.             if (string.IsNullOrEmpty (description))
  80.             {
  81.                 description = "Empty import";
  82.             }
  83.  
  84.             process.StandardInput.Write (description);
  85.             process.StandardInput.Flush ();
  86.             process.StandardInput.Close ();
  87.  
  88.             process.WaitForExit ();
  89.  
  90.             string output = process.StandardOutput.ReadToEnd ();
  91.             Console.Out.WriteLine ("Output:");
  92.             Console.Out.WriteLine (output);
  93.  
  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.             }
  101.  
  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:

*;/*Rss2.aspx;/Blog/Rss.aspx;/*/Blog.aspx;/Blog/$0/BlogEntry.aspx

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

*;Rss2.aspx;Rss.aspx;*/Blog.aspx;$0/BlogEntry.aspx

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 $_ }

VGhpcyBpcyBsaW5lIDE=

VGhpcyBpcyBsaW5lIDI=

U29tZSBtb3JlIHRleHQ=

 

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!

 

                                                Geoff



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:

Hi

I ask you some help

 

I have big problem of base64 decode

please help me.

 

please base64 decode source in VB.net 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,

 

                                                Geoff

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. 4 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 = "http://purl.org/rss/1.0/modules/content/";

        private const string WordpressNamespace = "http://wordpress.org/export/1.0/";

 

        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 ();

 

            return;

        }

 

        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);

            }

 

            return;

        }

 

        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);

 

            return;

        }

    }

}

 
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.

Bastards.



Posted by 'geoff' on Thursday, 09 November 2006. No comments.


Two weeks after Microsoft release an update to their Atlas project that, well, completely changes the client-side API, and I'm still catching up trying to re-write my code to get it back to the working state it used to be in.

And then they go and release .NET 3.0. There are no relevant breaking changes there, so you'd think it was good news, right? Well, to make up for the lack of breakage they've resorted to a few other nasty tricks.

I actually read the instructions telling me to uninstall the beta versions of the products, and what order to uninstall them. I even did exactly what I was told. Then I installed .NET 3.0. And that was fine. Then I tried to install the Visual Studio bits for .NET 3.0, and it failed, saying I had a beta of .NET 3.0 installed. The installer wouldn't continue.

So I used the tool Microsoft provide for just this situation. It said uninstall .NET 3.0 and then run the tool. I uninstalled .NET 3.0. I ran the tool. It said I had a .NET 3.0 beta installed (at least it's consistent), and it offered to uninstall it for me. So naturally I let it.

It did indeed uninstall the .NET 3.0 beta (this is the .NET 3.0 beta that I didn't have installed, remember). It also, quite thoughtfully, uninstalled a couple of gigabytes of MSDN documentation and my SQL Server Express installation too. I'm installing SQL Server Express again (aw crap, I've just realised I'm not installing SP1 with Advanced Services – I'm going to have to do it again…) I'm not sure what to do about the documentation – it looks thoroughly broken.

And they call this progress.



Posted by 'geoff' on Wednesday, 08 November 2006. No comments.


Did you ever have a program that was outside of your control, but that called another program in a way that wasn't quite right?
 
Well, that's the situation I was in.  A Crufty Old Source-Control Program (COSCP) uses its own built-in DIFF program, and I'd much rather use a tool like WinMerge for that kind of thing.  Naturally the COSCP doesn't allow you to change the program that's called, and since it uses such arcane practices as pipes ('|') to delimit file paths (and the parameters are in the wrong order) so I can't just replace their DIFF executable with WinMerge's.
 
So I wrote ParameterMapper.EXE.  It's a program that you can substitute in place of another EXE, that can call yet another EXE with the same or different parameters.
 
And there's more.  In a fit of boredom, I put in some CodeDOM gubbins that allows you to embed code to normalise parameters without having to plug in your own DLL.  It's kindof neat, if you like that sort of thing.
 
So, check ParameterMapper out.  It's free and the (GPL'd) source (.NET 2.0/VS2005) is there too if you want it.


Posted by 'geoff' on Tuesday, 17 October 2006. No comments.

1234

View my Technorati Profile.
RSS 2.0 Subscribe to the RSS 2.0 feed for Geoff's Blog.