Home | Random | Downloads | About | Hikes | WebCam | Search | Sitemap | Email | Feed

Shapefile Update

A few people have asked for 3D shape support in my ESRI Shapefile library. I've never got around to it, but CodePlex user ekleiman has forked a version in his ESRI Shapefile to Image Convertor that supports PointZ, PolygonZ and PolyLineZ shapes. If that's what you need please check it out.

Share:

Crushing PNGs in .NET

Crushing PNGs in .NET

I'm working on page speed and Google PageSpeed Insights is telling me that my PNGs are just way too large. Sadly .NET does not provide any way to optimize PNG images so there is no easy fix - just unmanaged libraries and command line tools.

I have an allergy to manual processes so I've lashed up some code to automatically find and optimize PNGs in my App_Data folder using PNGCRUSH. I can call CrushAllImages() to fix up everything or CrushImage() when I need to fix up a specific PNG. Code below:

public static void CrushAllImages()
{
    try
    {
        string appDataRoot = HostingEnvironment.MapPath("~/App_Data");
        if (appDataRoot == null)
        {
            return;
        }

        DirectoryInfo directoryInfo = new DirectoryInfo(appDataRoot);
        FileInfo[] pngs = directoryInfo.GetFiles("*.png", SearchOption.AllDirectories);
        foreach (FileInfo png in pngs)
        {
            CrushImage(png.FullName);
        }
    }
    catch (Exception ex)
    {
        //...
    }
}

public static void CrushImage(string fullPath)
{
    if (string.IsNullOrEmpty(fullPath))
    {
        return;
    }

    try
    {
        string markerPath = Path.ChangeExtension(fullPath, ".cng");
        if (File.Exists(markerPath))
        {
            return;
        }

        string crushExe = HostingEnvironment.MapPath("~/App_Data/pngcrush_1_7_77_w32.exe");

        ProcessStartInfo psi = new ProcessStartInfo(crushExe, string.Format(CultureInfo.InvariantCulture, "\"{0}\" \"{1}\"", fullPath, markerPath));
        psi.UseShellExecute = false;
        psi.CreateNoWindow = true;
        psi.LoadUserProfile = false;
        psi.WorkingDirectory = HostingEnvironment.MapPath("~/App_Data");

        Process p = Process.Start(psi);
        if (p == null)
        {
            throw new InvalidOperationException("No Process!");
        }
        p.WaitForExit();

        if (File.Exists(markerPath))
        {
            if (p.ExitCode == 0)
            {
                File.Copy(markerPath, fullPath, true);
                File.WriteAllText(markerPath, "Processed");
            }
            else
            {
                SiteLog.Log.Add(LogSeverity.Error, "CrushImage Failed (non-0 exit code) for " + fullPath);
                File.Delete(markerPath);
            }
        }
    }
    catch (Exception ex)
    {
       // ...
    }
}
Share:

Minify and inline CSS for ASP.NET MVC

ASP.NET has a CssMinify class (and a JavaScript variant as well) designed for use in the bundling pipeline. But what if you want to have your CSS minified and inline? Here is an action that is working for me (rendered into a style tag on my _Layout.cshtml using @Html.Action("InlineCss", "Home")).

public ActionResult InlineCss()
{
    BundleContext context = new BundleContext(
        new HttpContextWrapper(System.Web.HttpContext.Current), 
        BundleTable.Bundles, 
        "~/Content/css");
            
    Bundle cssBundle = BundleTable.Bundles.GetBundleFor("~/Content/css");
    BundleResponse response = cssBundle.GenerateBundleResponse(context);
           
    CssMinify cssMinify = new CssMinify();
    cssMinify.Process(context, response);

    return Content(response.Content);
}

Note that I'm using this to inline CSS for this blog. The pages are cached so I'm not worried about how well this action performs. My blog is also basically all landing pages so I'm also not worried about caching a non-inline version for later use, I just drop all the CSS on every page.

Share:

Personal Finger Daemon for Windows

Did you know that Windows still has a vestigial finger command with just about nothing left to talk to? One of my New Year's resolutions is to bring finger back and unlike the stalled webfinger project I need to make some progress. Here's some C# to run your own personal finger daemon... you just need to create a .plan file in your home directory (haven't done that for a while):

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace fingerd
{
    static class Program
    {
        private const int FingerPort = 79;
        private const int MaxFingerCommand = 256;
        private const string PlanFile = ".plan";

        private static readonly TcpListener _tcpListener = new TcpListener(
             IPAddress.Any, FingerPort);

        static void Main()
        {
            _tcpListener.Start();

            while (true)
            {
                TcpClient tcpClient = _tcpListener.AcceptTcpClient();
                Thread clientThread = new Thread(ClientThread);
                clientThread.Start(tcpClient);
            }
        }

        static void ClientThread(object client)
        {
            NetworkStream clientStream = null;
            TcpClient tcpClient = client as TcpClient;
            if (tcpClient == null) { return; }

            try
            {
                byte[] command = new byte[MaxFingerCommand];
                clientStream = tcpClient.GetStream();

                int read = clientStream.Read(command, 0, command.Length);
                if (read == 0) { return; }

                ASCIIEncoding asciiEncoding = new ASCIIEncoding();
                string commandText = asciiEncoding.GetString(command);

                int endOfCommand = commandText.IndexOf("\r\n"
                    StringComparison.InvariantCultureIgnoreCase);
                if (endOfCommand <= 0) { return; }

                string user = commandText.Substring(0, endOfCommand);
                if (string.Compare(user, Environment.UserName, 
                    StringComparison.InvariantCultureIgnoreCase) != 0) { return; }

                string planPath = Path.Combine(Environment.GetFolderPath(
                    Environment.SpecialFolder.UserProfile),
                    PlanFile);
                if (!File.Exists(planPath)) { return; }

                string plan = File.ReadAllText(planPath) + "\r\n";
                byte[] planBytes = asciiEncoding.GetBytes(plan);
                clientStream.Write(planBytes, 0, planBytes.Length);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                if (clientStream != null)
                {
                    clientStream.Close();
                }
                tcpClient.Close();
            }
        }
    }
}
Share:

Fix search on enter problem in BlogEngine.NET

Search on enter has been broken for a while in BlogEngine.NET (I'm running the latest 2.8.0.1 version). Finally got a chance to look at this today and there is a simple patch to the JavaScript to fix it. See the issue I just filed on CodePlex for details.

Share:

How to get SEO credit for Facebook Comments (the missing manual)

How to get SEO credit for Facebook Comments (the missing manual)

I've been using the Facebook Comments Box on this blog since I parted ways with Disqus. One issue with the Facebook system is that you won't get SEO credit for comments displayed in an iframe. They have an API to retrieve comments but the documentation is pretty light and so here are three critical tips to get it working.

The first thing to know is that comments can be nested. Once you've got a list of comments to enumerate through you need to check each comment to see if it has it's own list of comments and so on. This is pretty easy to handle.

The second thing is that the first page of JSON returned from the API is totally different from the other pages. This is crazy and can bite you if you don't test it thoroughly. For https://developers.facebook.com/docs/reference/plugins/comments/ the first page is https://graph.facebook.com/comments/?ids=https://developers.facebook.com/docs/reference/plugins/comments/. The second page is embedded at the bottom of the first page and is currently https://graph.facebook.com/10150360250580608/comments?limit=25&offset=25&__after_id=10150360250580608_28167854 (if that link is broken check the first page for a new one). The path to the comment list is "https://developers.facebook.com/docs/reference/plugins/comments/" -> "comments" -> "data" on the first page and just "data" on the second. So you need to handle both formats as well as the URL being included as the root object on the first page. Don't know why this would be the case, just need to handle it.

Last but not least you want to include the comments in a way that can be indexed by search engines but not visible to regular site visitors. I've found that including the SEO list in the tag does the trick, i.e.

<fb:comments href="..." width="630" num_posts="10">*Include SEO comment list here*</fb:comments>

I've included the source code for an ASP.NET user control below - this is the code I'm using on the blog. You can see an example of the output on any page with Facebook comments. The code uses Json.net.

FacebookComments.ascx:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="FacebookComments.ascx.cs" 
  Inherits="LocalControls_FacebookComments" %>

FacebookComments.ascx.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Caching;
using Newtonsoft.Json.Linq;

// ReSharper disable CheckNamespace
public partial class LocalControls_FacebookComments : System.Web.UI.UserControl
// ReSharper restore CheckNamespace
{
    private const string CommentApiTemplate = "https://graph.facebook.com/comments/?ids={0}";
    private const string CacheTemplate = "localfacebookcomments_{0}";
    private const int CacheHours = 3;

    public string PostUrl { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        try
        {
            if (!string.IsNullOrWhiteSpace(PostUrl))
            {
                string cacheKey = string.Format(CultureInfo.InvariantCulture, 
                    CacheTemplate, PostUrl);

                if (HttpRuntime.Cache[cacheKey] == null)
                {
                    StringBuilder commentBuilder = new StringBuilder();

                    string url = string.Format(CultureInfo.InvariantCulture,
                                               CommentApiTemplate,
                                               PostUrl);

                    while (!string.IsNullOrWhiteSpace(url))
                    {
                        string json;
                        using (WebClient webClient = new WebClient())
                        {
                            json = webClient.DownloadString(url);
                        }

                        // parse comments
                        JObject o = JObject.Parse(json);
                        if ((o[PostUrl] != null) &&
                            (o[PostUrl]["comments"] != null) &&
                            (o[PostUrl]["comments"]["data"] != null))
                        {
                            // first page
                            AppendComments(o[PostUrl]["comments"]["data"], commentBuilder);
                        }
                        else if (o["data"] != null)
                        {
                            // other pages
                            AppendComments(o["data"], commentBuilder);
                        }
                        else
                        {
                            break;
                        }

                        // next page URL
                        if ((o[PostUrl] != null) &&
                            (o[PostUrl]["comments"] != null) &&
                            (o[PostUrl]["comments"]["paging"] != null) &&
                            (o[PostUrl]["comments"]["paging"]["next"] != null))
                        {
                            // on first page
                            url = (string) o[PostUrl]["comments"]["paging"]["next"];
                        }
                        else if ((o["paging"] != null) &&
                                 (o["paging"]["next"] != null))
                        {
                            // on subsequent pages
                            url = (string) o["paging"]["next"];
                        }
                        else
                        {
                            url = null;
                        }
                    }

                    string comments = commentBuilder.ToString();

                    HttpRuntime.Cache.Insert(cacheKey,
                        comments,
                        null,
                        DateTime.UtcNow.AddHours(CacheHours),
                        Cache.NoSlidingExpiration);

                    LiteralFacebookComments.Text = comments;
                }
                else
                {
                    LiteralFacebookComments.Text = (string)HttpRuntime.Cache[cacheKey];
                }
            }
        }
        catch (Exception)
        {
            LiteralFacebookComments.Text = string.Empty;
        }
    }

    private static void AppendComments(IEnumerable comments, 
        StringBuilder commentBuilder)
    {
        foreach (JObject comment in comments)
        {
            // write comment
            commentBuilder.AppendFormat(CultureInfo.InvariantCulture,
                                        "
{0} ({1})

\r\n"
,
                                        comment["message"],
                                        comment["from"]["name"]);

            // also write any nested comments
            if ((comment["comments"] != null) && (comment["comments"]["data"] != null))
            {
                AppendComments(comment["comments"]["data"], commentBuilder);
            }
        }
    }
}
Share:

The curious case of the missing slugs (in BlogEngine.net 2.8)

2013-06-16 Update: There is now a patch for the issue discussed below.

I just upgraded to BlogEngine.net 2.8 as it contains a fix for broken links from Facebook. There were a couple of hitches that I'll share in case they help anyone else.

I messed up the first upgrade attempt because the updater utility updates the source folder (containing the newly downloaded 2.8 code) instead of the destination folder (containing the current version of your blog). This is a little odd and the result is I uploaded an unchanged instance and then embarrassingly complained the the Facebook bug hadn't been fixed. It had, just not in the folder I was expecting. I probably didn't pay enough attention to the instruction video.

Having got that out of the way I discovered that new posts were appearing with a bad link (to /.aspx instead of /blog-title.aspx). I rarely post using the editor as I have a home-grown post by email service running. After a bit of digging it turns out that prior to 2.8 you could leave the slug empty when creating a post but now this results in the bad link. Luckily there isn't much effort require to fix this, you just need to set the slug before saving the new post:

if (string.IsNullOrWhiteSpace(post.Slug))
{
    post.Slug = Post.GetUniqueSlug(post.Title, post.Id);
}

In the middle of playing with this my live site died and started returning a 500 error. No amount of uploading the working local copy would fix this. Happily Server Intellect have outstanding support and restored a working backup for me in the middle of the night. Thanks chaps!

Share:

Catfood: Earth for Android 1.10

Catfood Earth for Android 1.10

I’ve just released Catfood Earth for Android 1.10. You can control the center of the screen manually (the most requested new feature) and also tweak the transparency of each layer and the width of the terminator between day and night. It also starts a lot faster and has fewer update glitches. Grab it from Google Play if this looks like your sort of live wallpaper.

Share:

.NET 2.0 and Windows 8

Inexplicably .NET 2.0, 3.0 and 3.5 are not installed by default in Windows 8 and can’t be installed using the redistributables that worked with previous versions of Windows. You have to go digging in Windows Features to get anything older than 4.0.

Share:

Reviews and Links for August 2012

The Last Policeman by Ben H. Winters

The Last Policeman by Ben H. Winters

5/5

Stonking police procedural set in the months leading up to a global catastrophe.

 

Kill Decision by Daniel Suarez

Kill Decision by Daniel Suarez

5/5

Excellent techno-thriller. A little more serious and focused than Daemon and Freedom (TM). It's about a worst case drone scenario, ants, extra-special forces and some smart birds. Very good.

 

Links

Windows 8, Users 0? http://t.co/966Cuwjz

Bill Nye declares Todd Akin "fucking idiot"; issues debate challenge http://t.co/AZ3k55Y4 #fb

ITHCWY: Fight Facebook with Email: I was a little saddened to read today that Diaspora is transitioning over to… http://t.co/2G0pDdu0

Diaspora Founders To Move On, Handing Over Decentralized Social Network ‘To The Community’ http://t.co/KQGb2kpv -- sad, but not the future

RT @MargaretAtwood: Just used http://t.co/Nhna2CGO for gruesome printer problem: excellent, done in 10 mins! Tks to S H E F I N. Website ...

Check out Catfood Earth Live Wallpaper on Google Play! https://t.co/NTJQ1sYL

ITHCWY: Twenty-Four Hours with Twilio: I've wanted to play with Twilio's voice and SMS service for a while and… http://t.co/KOK0PG2M

Tuesdays http://t.co/BLNiCP3H

Twitter Cuts Off Tumblr's Ability to Find Friends http://t.co/1g3ZcClf

5 of 5 stars to The Last Policeman by Ben H. Winters http://t.co/9NF2nviH

XML: http://t.co/VadVt321 #rofl

Windows 8 Is Now Available For Developers (And For Everybody Else, There’s A 90-Day Free Trial, Too) http://t.co/iu1li6BV

Gotye's YouTube orchestra remix of "Somebody That I Used to Know" http://t.co/OUEXXltQ

ITHCWY: City by the Bay: View from Bernal Hill this afternoon. http://t.co/11cI3ctk

Nice panorama! Curiosity rover: Martian solar day 2 #360pano http://t.co/w1H2ocUm via @360cities

ITHCWY: Share a picture in MonoDroid: Here’s how to share a picture to Facebook, Twitter and so forth from… http://t.co/pByzvjmx

5 of 5 stars to Kill Decision by Daniel Suarez http://t.co/TKgUMNW0

BBC News - Mars rover makes first colour panorama http://t.co/fZ7u8smZ

How Apple and Amazon Security Flaws Led to My Epic Hacking http://t.co/BDmRAbm3

Via KQED Guides: Guide to Bay Area Tidepools: Where to Explore Amazing Marine Life | http://t.co/TDBRDnTD #todo @myEN

ITHCWY: Catfood: WebCams for Android: I’ve just released a WebCam app for Android. It’s based on WebCamSaver but… http://t.co/azZUwkkz

Pay for a new social network with no ads? https://t.co/2tox3c2y Anyone I know going to be on there? #fb

Curiosity http://t.co/D2cyWE66

BBC News - Photo shows Mars rover descent http://t.co/KfOv1qOF

Catfood WebCams for Android - Catfood Software http://t.co/VzeySq90 via @CatfoodSoftware

Check out Catfood WebCams on Google Play! https://t.co/VTU8YiBd

ITHCWY: Not a Private Key: When jarsigner says "Key Associated with [alias] not a private key" it almost certainly… http://t.co/3sk89ENV

ITHCWY: Sending email via GMail in C#/.NET using SmtpClient: I’ve stubbed my toe on this a couple of times, so here… http://t.co/QJ7YjcjI

Help end patent litigation insanity and tell your congress person to back SHIELD. http://t.co/27anadBt

ITHCWY: Support SHIELD–a small measure of patent sanity: A friend pointed me at the SHIELD (PDF) act today. This… http://t.co/ArXHgZ0e

ITHCWY: Thank you for choosing HSA Bank!: No, thank you HSA Bank for not giving me a choice and then cheekily… http://t.co/9torSXCq

Bill would force patent trolls to pay defendants’ legal bills | Ars Technica http://t.co/poB3zzlX +1, via @sr00t

What a happy coincidence. As well as #IPAday it's also goof off at work day: http://t.co/7AItItWq

Apparently it's #IPADay - luckily there's some @21stAmendment in the fridge. http://t.co/jyYPSePC

Share:

Fight Facebook with Email

I was a little saddened to read today that Diaspora is transitioning over to some form of community manged slow death. I joined a pod a while back and was pretty impressed with the design. It was very similar to Google+: clean, nice features, nobody home. 

I've also joined app.net. The concept here is a social network that you pay for, so the owners are aligned with the interests of the users and developers rather than advertisers and lame brands. I wish app.net well, but it's not the future. Best case (and it's not a bad one) it could be the new WELL - a community that people care enough about to pay for (I was on the WELL in the early 90's, splitting the tab with a friend so our handle was abft, account built for two). If that is the direction it goes in then simply having a slightly longer post limit than Twitter isn't really going to cut it. And cool as it might be most people aren't going to pay for a social network. 

Any attempt to displace Facebook has to solve the problem that anyone interested in sharing anything with anyone else is already using Facebook. The only platform that is in any sense comparable is email. So someone needs to make email into a social network.

This could be an interesting startup. Create some account - [email protected] - anything you send directly to that address is a post. Anyone you copy is a mention. Reply to a thread with this email address included and you're replying on the social network as well. Anyone copied on such an email gets invited to the network if they're not already.

You've got a killer viral component and an instant social network that is supported on every platform with no investment needed. Everyone has email, and everyone is a member as soon as they claim their email address or get included in a post. 

Maybe someone has tried this already and I just haven't seen it. I'm half tempted to have a crack at it myself. 

What would be more interesting would be layering a social protocol over email, and implementing that protocol by proxy on top of email providers that don't or won't support it. This creates a core social service practically out of thin air. Facebook and Twitter are the new AOL and CompuServe. There has to be a way to leverage email into a free and open alternative.

Share:

Twenty-Four Hours with Twilio

Twenty-Four Hours with Twilio

I've wanted to play with Twilio's voice and SMS service for a while and finally got the chance at an all night hackathon.

Twilio is almost perfect. Very easy to use, well documented and generous with a self service free trial. They give you enough rope to fully build out a client before you need to think about paying them.

I gave up on AT&T's platform after navigating a bazillion forms before discovering they wanted $99 before I could even get a taste.

The one hitch with Twilio is that the voice transcription they offer is appalling. Just absolute gibberish. Unless it's just my accent, or the hackathon whisky. Seems to be a common complaint though and so to take my IVR app further I'd need to bolt in another solution. A pity given how much Twilio gets right.

Share:

Share a picture in MonoDroid

Here’s how to share a picture to Facebook, Twitter and so forth from MonoDroid:

Java.IO.File cache = ExternalCacheDir;
if ((cache == null) || (!cache.CanWrite()))
{
    // no external cache
    cache = CacheDir;
}

Java.IO.File tempFile = new Java.IO.File(cache, "temp.jpg");
using (FileStream fileStream = File.OpenWrite(tempFile.AbsolutePath))
{
    _currentBitmap.Compress(Bitmap.CompressFormat.Jpeg, 85, fileStream);
}

Intent shareIntent = new Intent(Intent.ActionSend);
shareIntent.PutExtra(Intent.ExtraStream, Android.Net.Uri.FromFile(tempFile));
shareIntent.PutExtra(Intent.ExtraText, "Some text - appears in tweets, not on facebook"));
shareIntent.SetType("image/jpeg");

StartActivity(Intent.CreateChooser(shareIntent, "Share Image");

A fun mix of Java and C#. The directory got me to start with so check to see if the ExternalCacheDir is available and if not fall back to the internal CacheDir. Frustratingly Facebook doesn’t pick up on the text associated with an image regardless of the intent ExtraWhatever specified.

Share:

Catfood: WebCams for Android

Catfood WebCams for Android

I’ve just released a WebCam app for Android. It’s based on WebCamSaver but allows you to control the webcam – you tap the edges of the screen to pan, pinch to zoom in and out. A fun little time waster.

This is the first app I’ve released using Xamarin’s MonoDroid framework. This integrates nicely into Visual Studio and allows you to program an Android app in C#. This is fantastic for productivity and code reuse and I enjoyed the process a lot more than previous work I’ve done in Java / Eclipse. The main drawback is that the framework adds around 5MB (significant for mobile) and the documentation isn’t always the best, especially when you search for something and find out you’ve been dumped into iOS reference material. Digging around the sample code and cross-referencing the official Android documentation helps a lot. I’m going to take a stab at something a little more ambitious next…

Share:

Sending email via GMail in C#/.NET using SmtpClient

Gmail Logo

I’ve stubbed my toe on this a couple of times, so here is the magic incantation:

using (SmtpClient smtp = new SmtpClient())
{
    smtp.DeliveryMethod = SmtpDeliveryMethod.Network;
    smtp.UseDefaultCredentials = false;
    smtp.EnableSsl = true;
    smtp.Host = "smtp.gmail.com";
    smtp.Port = 587; 
    smtp.Credentials = new NetworkCredential("[email protected]""password");
    // send the email
}

Update 2015-02-11: A comment below from Shika Helmy suggests that adding a timeout might be helpful. Also note that if you have enabled two factor authentication for your Google Account you'll need to generate an app password in order to use basic authentication to the Gmail SMTP server.

Share:

Catfood: PdfScan 1.40

Catfood: PdfScan 1.40

Catfood PdfScan 1.40 is a small bug fix release. PdfScan converts documents to PDFs with the help of a flatbed or automatic document feeder (ADF) scanner.

Share:

Upgrading to BlogEngine.NET 2.5

Today I upgraded this blog to the latest and greatest version of BlogEngine.NET. Not entirely smooth sailing, so here are my notes for others (and the next time I have to do it):

The IsCommentsEnabled property BlogEngine.Core.Post has changed to HasCommentsEnabled. Not sure why this was worth changing but easy enough to fix.

BlogSettings.Instance.StorageLocation doesn't exist any more. After some digging it turns out that you need to use Blog.CurrentInstance.StorageLocation instead.

WidgetBase and WidgetEditBase in a couple of custom widgets complained that the type or namespace could not be found. This is fixed by adding a using statement for App_Code.Controls.

Updated 2012-03-11:

Getting the blog running locally was as usual only half the hassle. The next step is deploying to my hosting provider, Server Intellect. Things always start going wrong at this point. Luckily Server Intellect has some really great support staff and they respond quickly even on a Saturday night.

The first problem is that my backups were broken. Backups always succeed, restores always fail. After restoring my App_Data folder the last month of posts were missing. After digging for a bit it turned out that recent files were invisible over FTP but present in the control panel for the domain. My server had been migrated and some sort of permissions issue had broken access to new files. Not specifically a BlogEngine.net issue, but took a while to figure out and then for Server Intellect to fix.

Once the files were all there I uploaded and the blog itself was working fine, but the admin pages were screwy. It turns out that my server doesn't have ASP.NET MVC 3 installed. Server Intellect offered to migrate the server, but instead I copied System.Web.Mvc.dll to the Bin folder after finding a post on MVC 3 deployment from Scott Hanselman. I also needed to add a MIME type for .cshtml (text/html). With this in place the fancy new admin pages are up and running. 

Updated again, 2012-03-11:

Another namespace issue, ExtensionSettings in an extension doesn't resolve any more. Need to add a using statement for BlogEngine.Core.Web.Extensions. There are also some changes required to make an extension support multiple blogs.

Share:

Catfood.Shapefile 1.50

I've just released a small update to my C# Shapefile library on Codeplex. Catfood.Shapefile 1.50 fixes a couple of bugs related to metadata and adds the ability to access metadata records directly via IDataRecord. 

Share:

Moon on a Wire

Moon on a Wire

Testing posting by email with a picture of the moon from last night...

I've extended BlogEngine.NET to post by email. Not horrible for the very specific case of this blog and a short list of email clients. I shudder to think of extending it to the general emails and different templates.

Share:

Catfood.Shapefile 1.40

I’ve just released a small update to Catfood.Shapefile. Stephan Stapel, who implemented PolyLineM support, has contributed a patch that improves the class hierarchy. CodePlex user originSH suggested supporting the ACE driver for 64-bit systems. I’ve added a constructor overload that allows you to use predefined Jet and ACE connection strings or provide your own templates if necessary. Thanks to Stephan and originSH.

Catfood.Shapefile is a .NET library for enumerating ESRI shapefiles. I originally wrote the library to help me build some complex layers in Catfood Earth. Since then it’s picked up thousands of users and some really valuable suggestions and patches from the CodePlex community. I’m very glad a took a couple of hours to open source the library back in 2009.

Share:

PolyLineM support in Catfood.Shapefile

I’ve just updated Catfood.Shapefile, my ESRI Shapefile parser for .NET, with PolyLineM support thanks to a contribution from Stephan Stapel. The solution for the new version has also been updated to Visual Studio 2010.

Download Catfood.Shapefile.dll 1.30 from CodePlex.

Share:

Debugging Treasure Trove

Mark Jackson, my co-founder at Cucku, is blogging re-mastered debugging tips from StackHash at infopurge.tumblr.com. StackHash is now an open source project on CodePlex and all of the great content from the original site has been taken offline. This new project is a great resource for debugging on the Windows platform, especially post-mortem crash dump analysis. If that’s your thing do yourself a favor and subscribe to Mark’s blog.

Share:

Basic HTTP auth for an IIS hosted WCF 4 RESTful service

Wasted far too long on trying to get WCF to work with custom basic authentication this week. Custom in the sense that I need to look up the username and password in a database and not have IIS attempt to match the credentials to a Windows account. Given how well WCF 4.0 supports RESTful services in general it’s a bit shocking that basic auth over SSL isn’t supported out of the box. It seems like you should be able to derive and hook up a class from UserNamePasswordValidator, set the transport clientCredentialType to Basic and be ready to go. I’ve heard that this works for self-hosted services, but no dice in IIS.

Basic access authentication is a simple protocol and so in the end I added a helper method that checks for access (and in my case returns the user information for later use) at the start of each call into the service. It’s very simple:

  1. Check WebOperationContext.Current.IncomingRequest.Headers for an ‘Authorization’ header. If it’s there decode and validate the credentials.
  2. If the header is missing or the credentials are incorrect add the WWW-Authenticate header to the response - WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"myrealm\""); – and then throw a WebFaultException with a 401 Unauthorized status code.

This triggers a browser to prompt for your username and password and then try the request again. When calling the service in code you can add the ‘Authorization’ header preemptively and skip the 401 response entirely.

Share:

Reviews and links for August 2011

RESTful .NET by Jon Flanders

RESTful .NET by Jon Flanders

4/5

Great coverage of exposing and consuming a RESTful service using WCF. Note that you'll need the services of a good WCF book, this builds on existing WCF expertise and doesn't try that hard to bring you up to speed. Which isn't a bad thing, it keeps the book relatively short and focused. I'll be referring back to this one often.

 

Rule 34 by Charles Stross

Rule 34 by Charles Stross

4/5

Stross flips out concepts in a sentence that many SciFi authors would build an entire book around. It's a near-future police procedural set in Edinburgh. Twisted, tongue-in-cheek, profane and most excellent. The only miss is the assumption that people will use Wave in the near-future, let alone now. It's the first book of his that I've read... will be seeking out more soon.

 

The Information: A History, A Theory, A Flood by James Gleick

The Information: A History, A Theory, A Flood by James Gleick

4/5

Epic. A must read for cybernauts who may have forgotten their roots. Good for anyone else interested in what information actually is, and how pervasive information theory has become.

 

Links

- Password Strength from xkcd.com (Read this now, then change your passwords!).

- Baby sex blood tests 'accurate' from BBC News - Home (Bad news for girls...).

- Are your genes somebody else's property? from All Salon (More patent stupidity, this time genes (@myEV)).

- IE users have lower IQ says study from BBC News - Home (Highest IQ? Telnet to port 80 directly).

Share:

Convert BlogML comments to WXR for Disqus

I’ve just moved ITHCWY comments over to Disqus. BlogEngine.NET now supports Disqus out of the box, but doesn’t export comments to anything that Disqus is willing to eat. I’ve knocked up a quick converter that takes a full BlogML export from BlogEngine.NET (and at least in theory any other source of BlogML) and converts the comments to WXR. You can import the WXR file under the Generic option in Disqus.

The tool is a Windows console application that takes two parameters, the BlogML import file and the WXR output, i.e.:

BlogMLtoDisqus.exe C:\BlogML.xml C:\ForDisqus.wxr

It isn’t fancy and there is no error checking so it will either work or die horribly. If the latter, leave a comment and I’ll try to fix it for you.

Download BlogMLtoDisqus.exe. You’ll need to install .NET 4.0 as well if you don’t already have it.

Updated 2011-04-22: Added an optional third parameter that specifies the XML namespace for BlogML in case you need to override the default.

Share:

Merging Resource Dictionaries for fun and profit

Here are two scenarios where merged ResourceDictionary objects are the way forward.

I’m working on a WPF project that needs to be single instance. Heaven forbid that the WPF team should pollute the purity of their framework with support for this kind of thing (or NotifyIcon support but that’s another story) so I’m using the code recommended by Arik Poznanski: WPF Single Instance Application. I like this because it both enforces a single instance and provides an interface that reports the command line passed to any attempt to launch another instance.

An issue with using this code is that you need to write a Main function and so App.xaml is set to Page instead of Application Definition. Once you’ve done this the program works fine but the Visual Studio designer fails to load resources in UserControls (and in Windows containing those UserControls).

The fix is to factor all of the application level resources out into a separate ResourceDictionary (i.e. MergedResources.xaml). Once you’ve done this merge the new ResourceDictionary into App.xaml as follows:

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="MasterResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

Next, in each Window or UserControl reference the same ResourceDictionary:

<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="MasterResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>

The designer will now be able to find the correct resources for each UserControl and Window.

The second scenario is factoring resources and other Xaml into a DLL. To pull resources in from a referenced assembly you just need to use a Pack Uri when merging in the remote ResourceDictionary:

<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MyDll;component/ExternalResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

If you’re putting Windows and UserControls in the DLL use exactly the same approach to reference the resources using ResourceDictionary.MergedDictionaries and you’ll get designer support for these as well.

Share:

UAC shield icon in WPF

WPF: When it's good it’s very, very good and when it’s bad it’s like sautéing your own eyeballs.

When you’re about to launch a process that will trigger an elevation prompt it’s polite to decorate it with the little UAC shield so the user knows what to expect. Of course there’s no such capability in WPF, and WPF controls have no handles so you can’t use SendMessage / BCM_SETSHIELD as with Windows Forms.

System.Drawing.SystemIcons.Shield seems promising, but it returns the wrong icon on Windows 7 (at least in .NET 4).

SHGetStockIconInfo will allow you to get the correct icon, but isn’t supported on Windows XP. I’ve just added the necessary interop signatures for SHGetStockIconInfo to pinvoke.net so I won’t duplicate that code here. Once you have the interop you can get the correct icon as a BitmapSource using the following code:

BitmapSource shieldSource = null;

if (Environment.OSVersion.Version.Major >= 6)
{
    SHSTOCKICONINFO sii = new SHSTOCKICONINFO();
    sii.cbSize = (UInt32) Marshal.SizeOf(typeof(SHSTOCKICONINFO));

    Marshal.ThrowExceptionForHR(SHGetStockIconInfo(SHSTOCKICONID.SIID_SHIELD,
        SHGSI.SHGSI_ICON | SHGSI.SHGSI_SMALLICON,
        ref sii));

    shieldSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(
        sii.hIcon,
        Int32Rect.Empty, 
        BitmapSizeOptions.FromEmptyOptions());

    DestroyIcon(sii.hIcon);
}
else
{
    shieldSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(
        System.Drawing.SystemIcons.Shield.Handle,
        Int32Rect.Empty, 
        BitmapSizeOptions.FromEmptyOptions());
}
Share:

Geotagging posts in BlogEngine.NET

I've written an extension for BlogEngine.NET that automatically adds several different geographical tags to blog posts. I knocked this up for my Hikes blog. It might be useful for any blog where some of the posts are related to a real world location.

To get started download GeotagFromKML.zip (2.24 kb) and copy GeotagFromKML.cs to the App_Code\Extensions folder in your BlogEngine.NET instance.

The extension does two things. Firstly it looks for a link to a KML file when post is added or updated (it does this because each of my hike posts includes a Google Earth KML file for the hike). If a KML link is found then a paragraph is added to the post containing the longitude and latitude of the first coordinate in the KML file. The paragraph uses the Geo microformat. You can customize the text in settings for the extension. You can also regenerate by deleting the paragraph and saving the post. 

The second function is to add ICBM and Geo Tag META tags when serving a post that contains the geotagged coordinates. You can take advantage of this without linking to a KML file, just include a location like this in your post:

<span class="geo">
    <span class="latitude">37.754849</span>
    <span class="longitude">-122.446607</span>
</span>

Once you have geotagging up and running you might also want to add GeoURL to the list of ping services for your site.

Share:

Reboot computer in C# / .NET

.NET doesn’t support rebooting, logging off or shutting down your computer though a managed API. Searching for the best way to do this brings up three options: WMI, shutdown.exe and ExitWindowsEx.

I regard WMI as the last resort of the desperate. Weakly typed magic string juju.

Calling Process.Start(“shutdown.exe /r /t 0”) might work, but how would you know? And you’ve got the overheard of starting a new process just to accomplish a reboot. Lazy.

The best way to reboot is P/Invoke to ExitWindowsEx. Unfortunately there’s some really awful sample code out there which will either fail to do anything or mask any errors. I’ve included a drop-in class below that fixes these problems.

If you read all the way through the documentation for ExitWindowsEx you’ll find this:

To shut down or restart the system, the calling process must use the AdjustTokenPrivileges function to enable the SE_SHUTDOWN_NAME privilege. For more information, see Running with Special Privileges.

So just calling ExitWindowsEx won’t do anything. The sample code below adjusts the process token and then reboots (change the flags passed to ExitWindowsEx to shutdown instead, or to pass in a different reason). You’ll also get a Win32Exception if a failure occurs. Catch this, and you can tell the user that they need to reboot manually.

static class NativeMethods
{
    /// 

    /// Reboot the computer
    /// 

    public static void Reboot()
    {
        IntPtr tokenHandle = IntPtr.Zero;

        try
        {
            // get process token
            if (!OpenProcessToken(Process.GetCurrentProcess().Handle,
                TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES,
                out tokenHandle))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(),
                    "Failed to open process token handle");
            }

            // lookup the shutdown privilege
            TOKEN_PRIVILEGES tokenPrivs = new TOKEN_PRIVILEGES();
            tokenPrivs.PrivilegeCount = 1;
            tokenPrivs.Privileges = new LUID_AND_ATTRIBUTES[1];
            tokenPrivs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

            if (!LookupPrivilegeValue(null,
                SE_SHUTDOWN_NAME,
                out tokenPrivs.Privileges[0].Luid))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(),
                    "Failed to open lookup shutdown privilege");
            }

            // add the shutdown privilege to the process token
            if (!AdjustTokenPrivileges(tokenHandle,
                false,
                ref tokenPrivs,
                0,
                IntPtr.Zero,
                IntPtr.Zero))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(),
                    "Failed to adjust process token privileges");
            }

            // reboot
            if (!ExitWindowsEx(ExitWindows.Reboot,
                    ShutdownReason.MajorApplication | 
            ShutdownReason.MinorInstallation | 
            ShutdownReason.FlagPlanned))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(),
                    "Failed to reboot system");
            }
        }
        finally
        {
            // close the process token
            if (tokenHandle != IntPtr.Zero)
            {
                CloseHandle(tokenHandle);
            }
        }
    }

    // everything from here on is from pinvoke.net

    [Flags]
    private enum ExitWindows : uint
    {
        // ONE of the following five:
        LogOff = 0x00,
        ShutDown = 0x01,
        Reboot = 0x02,
        PowerOff = 0x08,
        RestartApps = 0x40,
        // plus AT MOST ONE of the following two:
        Force = 0x04,
        ForceIfHung = 0x10,
    }

    [Flags]
    private enum ShutdownReason : uint
    {
        MajorApplication = 0x00040000,
        MajorHardware = 0x00010000,
        MajorLegacyApi = 0x00070000,
        MajorOperatingSystem = 0x00020000,
        MajorOther = 0x00000000,
        MajorPower = 0x00060000,
        MajorSoftware = 0x00030000,
        MajorSystem = 0x00050000,

        MinorBlueScreen = 0x0000000F,
        MinorCordUnplugged = 0x0000000b,
        MinorDisk = 0x00000007,
        MinorEnvironment = 0x0000000c,
        MinorHardwareDriver = 0x0000000d,
        MinorHotfix = 0x00000011,
        MinorHung = 0x00000005,
        MinorInstallation = 0x00000002,
        MinorMaintenance = 0x00000001,
        MinorMMC = 0x00000019,
        MinorNetworkConnectivity = 0x00000014,
        MinorNetworkCard = 0x00000009,
        MinorOther = 0x00000000,
        MinorOtherDriver = 0x0000000e,
        MinorPowerSupply = 0x0000000a,
        MinorProcessor = 0x00000008,
        MinorReconfig = 0x00000004,
        MinorSecurity = 0x00000013,
        MinorSecurityFix = 0x00000012,
        MinorSecurityFixUninstall = 0x00000018,
        MinorServicePack = 0x00000010,
        MinorServicePackUninstall = 0x00000016,
        MinorTermSrv = 0x00000020,
        MinorUnstable = 0x00000006,
        MinorUpgrade = 0x00000003,
        MinorWMI = 0x00000015,

        FlagUserDefined = 0x40000000,
        FlagPlanned = 0x80000000
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct LUID
    {
        public uint LowPart;
        public int HighPart;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct LUID_AND_ATTRIBUTES
    {
        public LUID Luid;
        public UInt32 Attributes;
    }

    private struct TOKEN_PRIVILEGES
    {
        public UInt32 PrivilegeCount;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
        public LUID_AND_ATTRIBUTES[] Privileges;
    }

    private const UInt32 TOKEN_QUERY = 0x0008;
    private const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020;
    private const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
    private const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege";

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool ExitWindowsEx(ExitWindows uFlags, 
        ShutdownReason dwReason);

    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool OpenProcessToken(IntPtr ProcessHandle, 
        UInt32 DesiredAccess, 
        out IntPtr TokenHandle);

    [DllImport("advapi32.dll", SetLastError = true, CharSet=CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool LookupPrivilegeValue(string lpSystemName, 
        string lpName, 
        out LUID lpLuid);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CloseHandle(IntPtr hObject);

    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle,
        [MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges,
        ref TOKEN_PRIVILEGES NewState,
        UInt32 Zero,
        IntPtr Null1,
        IntPtr Null2);
}
Share:

Reviews and links for July 2010

Imperial Bedrooms by Bret Easton Ellis

Imperial Bedrooms by Bret Easton Ellis

4/5

Returns to the characters of Less Than Zero twenty five years later. I don't think it's a plot spoiler to say that they're not happy and well adjusted people. I found Glamorama to be pretty tedious and Lunar Park only marginally better. It was a huge relief that Imperial Bedrooms just flows. It's a welcome return to his earlier narrative style. Dread and paranoia are visceral presences from the start and then layers of fear and horror build until it can't get any worse and then somehow does. Brilliant.

 

Blue Ocean Strategy: How to Create Uncontested Market Space and Make Competition Irrelevant by W. Chan Kim

Blue Ocean Strategy: How to Create Uncontested Market Space and Make Competition Irrelevant by W. Chan Kim

2/5

Given its pedigree it's missing two segments - the yellow ocean (no competition, no customers) and the purple ocean (high competition, no customers). They must be saving those for a sequel. I read this because a few people had recommended it and if you think the ideal market to play in is one with no differentiation and high competition then it's a must read. Otherwise the only real value is being conversant with the buzz word. Evaluating past successes with 20/20 hindsight and talking about their 'blue ocean strategy' is a classic business book selection bias. If you learn anything from the case studies it should be that breakout innovation doesn't come from your ocean, hedgehog principle or current cheese location.

 

61 Hours (Jack Reacher Series, #14) by Lee Child

61 Hours (Jack Reacher Series, #14) by Lee Child

3/5

Well constructed if average plot. This is the Empire Strikes Back of Reacher novels and ends on a bit of a cliffhanger - the next in the series is out later this year and hopefully picks up the pace a bit.

 

Professional C# 4 and .NET 4 by Christian Nagel

Professional C# 4 and .NET 4 by Christian Nagel

4/5

I own the 2005 and 2008 flavors of this book as well. It's the best overall C# reference I've found and this 2010 version is a welcome update. As with the 2008 book it could use a better guide to new features, but still very highly recommended.

 

Ina May's Guide to Childbirth by Ina May Gaskin

Ina May's Guide to Childbirth by Ina May Gaskin

3/5

It's a somewhat troublesome mix of advice and propaganda. The advice seems mostly solid, practical and grounded in a great deal of experience. The book ends with the most important - don't think that your body is a lemon, pregnancy isn't a disease, you can do it. Ina May's statistics from "The Farm" are compelling as well, but the birth stories are a bit far out. They typically sound like: 'Sunflower, hanging from the birthing gallows while member of the Farm suck her nipples and I bring her to repeated orgasm, didn't even notice that her baby had been born'. For most people there's probably a middle ground between technocratic doctors and hippie midwives. When the book veers into propaganda it seems there's no anecdotal story too weird to make the case for natural childbirth and no study rigorous enough to suggest that there might be nothing to this modern medicine fad. Some suggestions - like that obstetricians don't believe that nutrition has a role in healthy pregnancy - are just so ridiculous that they case doubt on the rest of the book. And yet, her statistics are so very good while US hospitals force you into a caesarean section to prevent lawsuits and not miss happy hour. I guess the only conclusion to reach is to give birth in The Netherlands and then move to Sweden to take advantage of their twenty year maternity leave...

 

Links

- Frogger from xkcd.com (don't miss the alt text).

- Say fat not obese, urges minister from BBC News - Home (also 'a bit poorly' rather than 'cancerous').

- MoD 'must not live beyond means' from BBC News - Home (Easy fix... bring troops home and send National Audit Office to scold Iraq and Afghanistan into submission.).

- Vatican mulls sex abuse of impaired adults from All Salon (Hint: if you don't know the number for your local police department 112 will work on most mobile phones.).

- Call for school rugby scrum ban from BBC News | News Front Page | World Edition (Where was Professor Pollock when I was at school?).

- Grandmothers link orcas to humans from BBC News | News Front Page | World Edition (Could it be that the mothers also have mothers? Like necessarily?).

Share:

WPF commands with nested focus scope

Here's a frustrating WPF scenario — you use the ApplicationCommands class to add Cut, Copy and Paste commands to toolbar and then put a TextBox on another toolbar. Click in the TextBox and the commands remain disabled. WTF, WPF?

The problem is with focus scopes. Your window is a focus scope and so are any menus or toolbars. This has the desirable property of allowing commands to target the control you were in immediately before invoking the command. You want paste to target the text box you're editing, not the menu item or button you clicked to request the paste.

So far so good. The problem is that the commanding system isn't smart enough to target the control with keyboard focus if it's in a nested focus scope. Remember that the window itself is a focus scope so our TextBox in a ToolBar (also a focus scope) is nested and immune to commands from our menu or toolbar.

Here's a simple window that demonstrates the problem:

<Window x:Class="WpfFocusScopeTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">

    <Window.CommandBindings>
        <CommandBinding Command="Paste" x:Name="bindingPaste" PreviewCanExecute="bindingPaste_PreviewCanExecute" />
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" /><RowDefinition Height="Auto"  /><RowDefinition />
        </Grid.RowDefinitions>
        
        <Menu>
            <MenuItem Header="Edit"><MenuItem Command="Paste" Name="menuItemPaste"/></MenuItem>
        </Menu>
        
        <ToolBarTray Grid.Row="1">
            <ToolBar Band="0" BandIndex="0">
                <Button Command="Paste" Name="buttonPaste">Paste</Button>
            </ToolBar>
            <ToolBar Band="0" BandIndex="1">
                <TextBox Width="100" />
            </ToolBar>
        </ToolBarTray>

        <DockPanel Grid.Row="2">
            <TextBox />
        </DockPanel>

    </Grid>
</Window>

Ignore the PreviewCanExecute handler for now. If you run this window and click in the main TextBox the paste button and menu item are enabled. Click in the toolbar TextBox and pasting isn't an option. Well, Ctrl-V still works and there's a context menu but you know what I mean.

The problem can be fixed by adding a command binding for ApplicationCommands.Paste and handling the PreviewCanExecute event:

public partial class MainWindow : Window
{
    private DependencyObject _menuFocusScope;
    private DependencyObject _toolbarFocusScope;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        _menuFocusScope = FocusManager.GetFocusScope(menuItemPaste);
        _toolbarFocusScope = FocusManager.GetFocusScope(buttonPaste);
    }  

    private void bindingPaste_PreviewCanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        IInputElement keyboardFocus = Keyboard.FocusedElement;

        if ((keyboardFocus != null) && (keyboardFocus != this))
        {
            DependencyObject pasteTargetAsDependencyObjeect = 
                keyboardFocus as DependencyObject;
            if (pasteTargetAsDependencyObjeect != null)
            {
                DependencyObject targetFocusScope = 
                    FocusManager.GetFocusScope(pasteTargetAsDependencyObjeect);

                if ((targetFocusScope != _menuFocusScope) && 
                    (targetFocusScope != _toolbarFocusScope) )
                {
                    menuItemPaste.CommandTarget = keyboardFocus;
                    buttonPaste.CommandTarget = keyboardFocus;
                }
            }
        }
    }
}

When the window loads we're making note of the focus scopes for the toolbar and menu. Then when PreviewCanExecute fires we check to see if the element with the keyboard focus is in a different focus scope (and also that the window doesn't have keyboard focus). We then set the CommandTarget for the menu item and button to the element that has keyboard focus.

A handler isn't required for CanExecute as the command will take care of this with respect to the new CommandTarget.

Run the window again and you'll see that the paste button is enabled for both of the TextBox controls. When you click the button (or menu item) our PreviewCanExecute handler ignores the new keyboard focus and the command is sent to the desired control. 

One drawback of this approach is that keyboard focus isn't returned to the TextBox after the command executes. The CommandTarget remains in place so you can keep pasting and the command remains enabled but you lose the visual cue that lets you know where the target is. I haven't figured out a clean approach to this yet. When I do, I'll update this post. Better yet, if you've figured it out leave a comment.

Share:

Converting Blogger ATOM export to BlogML

I'm slowly converting a number of blogs from Blogger to BlogEngine.NET. The least fun part is dealing with the Blogger export file. For this blog I used a Powershell script but had problems with comments not exporting correctly and it was quite painful to fix everything up. Blogger allows you to export a copy of your blog using ATOM, however BlogEngine.NET (and other tools) speak BlogML.

I've just released a command line tool that takes the ATOM format Blogger export and converts it to BlogML. You can download Blogger2BlogML from Codeplex. The tool uses .NET 4.0 (client profile) so you'll need to install this if you don't already have it. If you give Blogger2BlogML a try let me know how you get on. 

Share:

ESRI Shapefile Library Update

I've just released a small update for my ESRI Shapefile Reader project on Codeplex. The only change is a patch from SolutionMania that fixes a problem when the shapefile name is also a reserved name in the metadata database. The patch escapes the name preventing an exception from being thrown.

Catfood.Shapefile.dll is a .NET 2.0 forward only parser for reading an ESRI Shapefile. Download 1.20 from Codeplex.

Share:

Scanning from the ADF using WIA in C#

Scanner ready for WIA image acquisition

I've been going nuts trying to scan from the document feeder on my Canon imageClass MF4150. Everything worked as expected from the flatbed, no dice trying to persuade the ADF to kick in. I found some sample code but it was oriented towards devices that can detect when a document is available in the feeder. Evidently my Canon doesn't expose this and so needs to be told the source to use.

The way to do this is to set the WIA_DPS_DOCUMENT_HANDLING_SELECT property to FEEDER. You then read WIA_DPS_DOCUMENT_HANDLING_STATUS to check that it's in the right mode and initiate the scan. This did not work for toffee.

After much experimentation I discovered a solution. I had been setting device properties and then setting item properties before requesting the scan. Switching the order - item then device - made everything work.

Here's the function to scan one page:

private XImage ScanOne()
{
    XImage ximage = null;

    try
    {
        // find our device (scanner previously selected with commonDialog.ShowSelectDevice)
        DeviceManager manager = new DeviceManager();
        DeviceInfo deviceInfo = null;
        foreach (DeviceInfo info in manager.DeviceInfos)
        {
            if (info.DeviceID == _deviceId)
            {
                deviceInfo = info;
            }
        }

        if (deviceInfo != null)
        {                    
            Device device = deviceInfo.Connect();
            CommonDialog commonDialog = new CommonDialog();

            Item item = device.Items[1];
            int dpi = 150;

            // configure item
            SetItemIntProperty(ref item, 6146, 2); // greyscale
            SetItemIntProperty(ref item, 6147, dpi); // 150 dpi
            SetItemIntProperty(ref item, 6148, dpi); // 150 dpi
            SetItemIntProperty(ref item, 6151, (int)(dpi * _width)); // scan width
            SetItemIntProperty(ref item, 6152, (int)(dpi * _height)); // scan height
            SetItemIntProperty(ref item, 4104, 8); // bit depth

            int deviceHandling = _adf ? 1 : 2; // 1 for ADF, 2 for flatbed

            // configure device
            SetDeviceIntProperty(ref device, 3088, deviceHandling); 
            int handlingStatus = GetDeviceIntProperty(ref device, 3087);

            if (handlingStatus == deviceHandling)
            {
                ImageFile image = commonDialog.ShowTransfer(item, formatJpeg, true);

                // save image to a temp file and then load into an XImage
                string tempPath = System.IO.Path.GetTempFileName();
                File.Delete(tempPath);
                tempPath = System.IO.Path.ChangeExtension(tempPath, "jpg");
                image.SaveFile(tempPath);
                ximage = XImage.FromFile(tempPath);

                _tempFilesToDelete.Add(tempPath);
            }
        }
    }
    catch (COMException ex)
    {
        ximage = null;

        // paper empty
        if ((uint)ex.ErrorCode != 0x80210003)
        {
            throw;
        }
    }

    return ximage;
}

A few notes — XImage is a type from PDFSharp. I wrote this as part of a PDF scanner that I'll post next so the scanned images are saved and then loaded into an XImage for rendering to the PDF document. The magic numbers come from WiaDef.h in the Platform SDK. If the ADF is out of pages this method sets the return image to null and eats the exception. This is because the function is called repeatedly to scan in pages until the ADF is empty if _adf is true (otherwise it grabs one image from the flatbed). 

If you've been banging your head against a wall trying to get WIA to work with a document feeder I hope this helps.

Updated 2015-05-20: Full source code at https://github.com/abfo/pdfscan

Share:

Use WPF Dispatcher to invoke event handler only when needed

After floundering a bit with the WPF Dispatcher I've come up with a simple way to make sure an event handler executes on the UI thread without paying the overhead of always invoking a delegate.

void someEvent_Handler(object sender, SomeEventEventArgs e)
{
    if (this.Dispatcher.CheckAccess())
    {
        // do work on UI thread
    }
    else
    {
        // or BeginInvoke()
        this.Dispatcher.Invoke(new Action(someEvent_Handler), 
            sender, e);
    }
}

This has the benefit (for me at least) of being very easy to remember. Hook up the event handler and then if there's a chance it could be called from a different thread wrap it using the pattern above. It's easier to read than an anonymous delegate and much faster than defining a specific delegate for the event in question.

I haven't tested the various methods to see which is the fastest yet… will get round to this at some point.

Share:

Reviews and links for April 2010

The Spire by Richard North Patterson

The Spire by Richard North Patterson

3/5

A good enough holiday read and nice to see Patterson return to a straight psychological thriller rather than the last few OpEds loosely wrapped with some plot.

 

Advanced .NET Debugging (Addison-Wesley Microsoft Technology Series) by Mario Hewardt

Advanced .NET Debugging (Addison-Wesley Microsoft Technology Series) by Mario Hewardt

5/5

Comprehensive introduction to low level .NET debugging - when you need to fire up WinDbg to check out the state of the managed heap, or debug a crash dump from the field you'll find this book invaluable. I wish it had been available when I started figuring out how to use SOS.

 

The Complete Stories of J. G. Ballard by J.G. Ballard

The Complete Stories of J. G. Ballard by J.G. Ballard

5/5

Wonderful collection of all of Ballard's short stories. It's a huge book with surprisingly few duds. My favorites include The Illuminated Man, clearly the inspiration for The Crystal World, which includes meaning bombs like "It's almost as if a sequence of displaced but identical images were being produced by refraction through a prism, but with the element of time replacing the role of light." and The Ultimate City (which isn't using ultimate in the sense of being good...). I've read most of Ballard's novels but not many of the short stories before. They're well worth the time.

 

Links

- Microsoft Agrees With Apple And Google: “The Future Of The Web Is HTML5″ from TechCrunch (Which makes it all the more tragic that a huge number of clients will still be running IE6 :().

- Comedian criticises BBC 'rebuke' from BBC News | News Front Page | World Edition (The problem isn't that it was anti-Semitic, it's that it wasn't funny.).

- UK 'has a high early death rate' from BBC News | News Front Page | World Edition (That'll be the deep fried mars bars and chips.).

- Oklahoma, where women's rights are swept away from All Salon (Competing with AZ to be the most fucked up state? Sigh :().

- Cameras capture 'Highland tiger' from BBC News | News Front Page | World Edition (Tabbs was bigger than that (a house cat)).

- MI5 dumps staff lacking IT skills from BBC News | News Front Page | World Edition (MI5 has staff without computer skills?).

- The Internet Provides. from jwz (Disturbing).

- Who Really Spends The Most On Their Military? from Information Is Beautiful (Click through to the Guardian blog post, interesting reading.).

Share:

XamlParseException and 256x256 icons

When testing out a WPF app on XP I got an unhelpful XamlParseException error report. 

I was a little puzzled because I was hooking up error reporting in App.xaml.cs:

App.Current.DispatcherUnhandledException += 
    new DispatcherUnhandledExceptionEventHandler(Current_DispatcherUnhandledException);
AppDomain.CurrentDomain.UnhandledException += 
    new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

My error handler was attempting to create a XAML window to report the error, and evidently this was bombing out as well triggering the good doctor Watson. I added a MessageBox call instead and discovered that the XamlParseException was wrapping a FileFormatException and the stack trace indicated that the problem was with setting the icon for the window. After removing the icon the app started up fine. Weird.

It turns out that WPF chokes on a compressed 256x256 icon on XP and Vista (Windows 7 seems to cope fine). Saving the icon without compression fixes the problem. I use IcoFX and you can set this at Options -> Preferences -> Options -> Compress 256x256 images for Windows Vista. Of course the consequence is that the icon is a couple of hundred kilobytes larger.

Share:

Space and multibyte character encoding for posting to Twitter using OAuth

I've spent the last day learning how to use OAuth and XAuth to post to Twitter. There are rumblings that Twitter will start to phase out basic authentication later this year, and more importantly you can only get the nice “via...” attribution if you use OAuth (for new apps, old ones are grandfathered in).

I coded up my own OAuth implementation, referring to Twitter Development: The OAuth Specification on Wrox and the OAuthBase.cs class from the oauth project on Google Code. Both are great references, but both fail with multibyte characters. The problem is that each byte needs to be separately escaped. OAuthBase.cs encodes characters as ints rather than breaking out the bytes and the Wrox article incorrectly suggests using Uri.EscapeDataString(). 

Here's a method to correctly encode parameters for OAuth:

public static string OAuthUrlEncode(string s)
{
    if (string.IsNullOrEmpty(s))
    {
        return string.Empty;
    }
    else
    {
        StringBuilder sb = new StringBuilder(s.Length);

        for (int i = 0; i < s.Length; i++)
        {
            if (NoEncodeChars.IndexOf(s[i]) == -1)
            {
                // character needs encoding
                byte[] characterBytes = Encoding.UTF8.GetBytes(s[i].ToString());
                for (int b = 0; b < characterBytes.Length; b++)
                {
                    sb.AppendFormat(CultureInfo.InvariantCulture,
                    "%{0:X2}",
                    characterBytes[b]);
                }
            }
            else
            {
                // character is allowed
                sb.Append(s[i]);
            }
        }

        return sb.ToString();
    }
}

NoEncode chars is a list of the permitted characters:

private const string NoEncodeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

An impact of this encoding is that spaces must be encoded as %20 rather than plus. I was worried that each space would end up counting as three characters towards the 140 character limit. I tested this and it isn't true, so use HttpUtility.UrlEncode() to calculate the number of characters in the post OAuthUrlEncode() or similar to actually encode post parameter.

Share:

BlogEngine.NET most popular pages widget using Google Analytics

I finished off my BlogEngine.NET migration yesterday missing a couple of useful sections from the previous incarnation of this blog. The first is a list of the most popular posts based on Google Analytics data. I've just finished porting this from a UserControl to a widget for BlogEngine.NET. To use this just download and extract this zip file to your widgets directory:

MostPopularFromGA.zip (5.22 kb)

You can see the widget in action under the Most Popular heading to the left if you're reading this post on the blog.

Most of the settings should be pretty obvious. The Google Analytics profile is the exception. This isn't the ID included in your tracking code. To get the profile log in to Google Analytics and click Edit next to the profile you want to use. At the top of the page you'll see a Profile ID. You need to use this number prefixed by ga: (i.e. ga:1234567). Once you have this and your account credentials entered you should be up and running.

The Post must match settings is a regular expression used to filter the Google Analytics report to only include blog posts. The default value corresponds to a default BlogEngine.NET install and only includes pages that start .../post/ (the regular expression is ^/post/). If your posts are under .../blog/posts/ then just update accordingly (^/blog/posts/). If you want to include the most popular pages regardless of the path just leave this setting blank. 

If you have any questions or feature requests leave a comment below. 

Share:

Migrating from Blogger to BlogEngine.NET

(Update June 22, 2010: I've released a tool, Blogger2BlogML, that converts Blogger's ATOM export file to BlogML. I ended up doing this because of problems with comments when I migrated this blog — I had to fix these up manually which was painful. I'm now working on some larger blogs where this would be impossible…)

In January I got an email from Blogger announcing that they're killing FTP support. Apparently only 0.5% of Blogger blogs are published using FTP and it's a huge pain to support, mainly because many hosting providers restrict FTP access to certain IP addresses and if the Google servers running Blogger that moment aren't listed it's technical support time. 

Fair enough, but a bit painful for me as I have five blogs running on top of Blogger. I need FTP publishing as the templates I use end up running as ASP or ASP.NET pages. I Thought He Came With You is the first to move - if you're reading this post then it's up and running on BlogEngine.NET. This is an open source ASP.NET blogging platform. If it works out for this blog over the next month I'll start migrating the others. 

Getting up and running with BlogEngine.NET is easy enough - download the latest release and follow the getting started guide. I added the default install to a new Visual Studio web site project and was able to run it fine in the development server, no need to configure IIS. 

The challenge was moving posts and comments from Blogger into BlogEngine.NET. BlogEngine.NET happily imports and exports BlogML, Blogger spits out it's own Atom export format

Luckily Aaron Lerch has knocked up a PowerShell script to export Blogger to BlogML. This takes your Blogger ID as a parameter and exports all the blogs associated with that ID. You can just pull the <blog> element you need out of the export if necessary. If you use this tool follow the syntax closely from the example on the post. If you're new to PowerShell run this as administrator to start with and enter "Set-ExecutionPolicy RemoteSigned" to give permission to run the script. Then exit and run a non-admin instance of PowerShell to run the script.

After getting the export BlogEngine.NET refused to import it. I kept getting an invalid username or password error from the BlogML importer. Digging into the code a bit I found that a crash was occurring in api/BlogImporter.asmx when a blank category was passed to AddCategories(). I patched this function to skip blank/null categories and was then able to import successfully. I added a bug report (11789) for the problem so please vote for it if you hit the same issue.

The blog was in pretty good shape after the import. Categories were lost so I had to create them again in BlogEngine.NET and then apply them to each post. I also found that the date for comments had been set to the date they were imported. This can be fixed by editing each post directly (the files in App_Data\posts). I fixed the timestamps and also added back the URL for each comment where I had one from Blogger. I also edited my own comments to match my new BlogEngine.NET account so they got decorated as "by the author".

At this point the blog was good to go, but using the default template.

I've now whipped up a new template to match the old blog - I was pleasantly surprised by how easy this was to do. At this point I Thought He Came With You is going live on BlogEngine.NET. 

Almost everything is working at this point. I had a couple of user controls I need to move over - one shows the number of people who haven't visited the blog, the other figures out the most popular posts using the Google Analytics API. I'll see if I can implement at least the latter as a Widget so that other people can use it - watch this space (Update - now available here). 

Overall, I'm impressed. I resisted the urge to just write my own blogging platform which is a testament to how easy it is to both get BlogEngine.NET running and to customize the look and feel. Now I just need to work up the enthusiasm to do the same for my remaining four Blogger based blogs...

Share:

Catfood.Shapefile.dll 1.10

I've just released v1.10 of my ESRI Shapefile Reader (Catfood.Shapefile.dll). This is a .NET 2.0 forward only parser for reading shapefile content.

Sharon Tickell was kind enough to report two bugs with suggested updates (10185 and 10186). These have both been fixed in 1.10.

While working on these fixes I also discovered that there are no 64-bit Jet drivers (since releasing the first version I've upgraded to a 64-bit box for development). This is an easy fix, just target any application using Catfood.Shapefile.dll at x86. I've updated the demo application accordingly.

Download Catfood.Shapefile.dll from Codeplex.

Share:

Ambient Orb Controller .NET Library

The Ambient Orb is an LED illuminated globe designed to display subtle information - stock market prices, weather, etc. Normally the Orb is controlled via the pager network but you can buy (or build) a developer module and connect the Orb via your serial port.

Ambient Orb

I've used my Orb via both pager and serial port for a number of applications. I've just released a library - Ambient Orb Controller - on CodePlex that supports both methods of control. I wrote the library to make it easier for me to gin up new Orb applications. If you use an Orb let me know what you come up with. Two of my favorites are using the Orb for continuous integration and as a music visualizer.

Share:

orb, .NET, C#,

ESRI Shapefile Reader in .NET

I've just released a .NET library for parsing ESRI shapefiles - see ESRI Shapefile Reader on Codeplex. The library and source code are available under the Microsoft Public License.

A Shapefile is actually at least three files: a main file containing shape data (*.shp), an index file for locating shape records in the main file (*.shx) and a database of metadata for each shape (*.dbf) in dBASE format.

I ended up writing the library in order to convert Eric Muller's time zone shapefile into a format I could use with Catfood Earth. I found other libraries that could read shape data but not metadata, or provided a very thin .NET wrapper on top of unmanaged code and so I decided that a fully managed library could be useful.

The library - Catfood.Shapefile.dll - provides read-only, forward-only access to shapes and shape metadata. Currently all 2D shapes are supported: Null, Point, MultiPoint, PolyLine and Polygon. I might add additional types in the future, or if you have a pressing need it would be easy to extend the library by looking at an existing shape subclass and the shapefile specification (PDF).

See the Codeplex project for sample code and documentation.

Share:

Fastest image merge (alpha blend) in GDI+

Blended image from Catfood Earth

I've been experimenting with the best way to merge two images in C#. That is, paint one image on top of another with some level of transparency as opposed to using one color as a transparency mask. I tried three candidates, all included below:

SimpleBlend is the naive approach using GetPixel and SetPixel to add the desired alpha value to the second image before painting it on top of the first.

MatrixBlend configures a ColorMatrix to specify the desired alpha and then paints the second image on to the first using the matrix.

ManualBlend locks and directly manipulates the image data. This uses pointers and so introduces unsafe code (it's possible to marshal the image data into a managed array but I wanted to look at the performance with raw access).

I tested each approach ten times with a couple of large JPEG images. The results are:

SimpleBlend: 17.69 seconds
MatrixBlend: 0.74 seconds
ManualBlend: 1.13 seconds

I expect ManualBlend could be optimized further but both this and MatrixBlend are an order of magnitude faster than the naive approach. I'll be using MatrixBlend as no unsafe code is required.

private static void SimpleBlend(string image1path, string image2path, byte alpha)
{
    using (Bitmap image1 = (Bitmap)Bitmap.FromFile(image1path))
    {
        using (Bitmap image2 = (Bitmap)Bitmap.FromFile(image2path))
        {
            // update the alpha for each pixel of image 2
            for (int x = 0; x < image2.Width; x++)
            {
                for (int y = 0; y < image2.Height; y++)
                {
                    image2.SetPixel(x, y, Color.FromArgb(alpha, image2.GetPixel(x, y)));
                }
            }

            // draw image 2 on image 1
            using (Graphics g = Graphics.FromImage(image1))
            {
                g.CompositingMode = CompositingMode.SourceOver;
                g.CompositingQuality = CompositingQuality.HighQuality;

                g.DrawImageUnscaled(image2, 0, 0);
            }
        }
    }
}

private static void MatrixBlend(string image1path, string image2path, byte alpha)
{
    // for the matrix the range is 0.0 - 1.0
    float alphaNorm = (float)alpha / 255.0F;
    using (Bitmap image1 = (Bitmap)Bitmap.FromFile(image1path))
    {
        using (Bitmap image2 = (Bitmap)Bitmap.FromFile(image2path))
        {
            // just change the alpha
            ColorMatrix matrix = new ColorMatrix(new float[][]{
                new float[] {1F, 0, 0, 0, 0},
                new float[] {0, 1F, 0, 0, 0},
                new float[] {0, 0, 1F, 0, 0},
                new float[] {0, 0, 0, alphaNorm, 0},
                new float[] {0, 0, 0, 0, 1F}});

            ImageAttributes imageAttributes = new ImageAttributes();
            imageAttributes.SetColorMatrix(matrix);

            using (Graphics g = Graphics.FromImage(image1))
            {
                g.CompositingMode = CompositingMode.SourceOver;
                g.CompositingQuality = CompositingQuality.HighQuality;

                g.DrawImage(image2, 
                    new Rectangle(0, 0, image1.Width, image1.Height), 
                    0, 
                    0, 
                    image2.Width, 
                    image2.Height, 
                    GraphicsUnit.Pixel, 
                    imageAttributes);
            }
        }
    }
}

private static void ManualBlend(string image1path, string image2path, byte alpha)
{
    // percentage of destination and source image
    float alphaDst = (float)alpha / 255.0F;
    float alphaSrc = 1.0F - alphaDst;

    using (Bitmap image1 = (Bitmap)Bitmap.FromFile(image1path))
    {
        Rectangle imageRect = new Rectangle(0, 0, image1.Width, image1.Height);
        BitmapData image1Data = image1.LockBits(imageRect, ImageLockMode.ReadWrite,
            PixelFormat.Format32bppArgb);
                
        using (Bitmap image2 = (Bitmap)Bitmap.FromFile(image2path))
        {
            BitmapData image2Data = image2.LockBits(imageRect, ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb);

            unsafe
            {
                uint* image1Raw = (uint*)image1Data.Scan0.ToPointer();
                uint* image2Raw = (uint*)image2Data.Scan0.ToPointer();
                int stride = image1Data.Stride / sizeof(uint);
                int currentPixel;
                uint image1Pixel, image2Pixel;
                uint srcRed, dstRed, finRed;
                uint srcGreen, dstGreen, finGreen;
                uint srcBlue, dstBlue, finBlue;

                for (int x = 0; x < imageRect.Width; x++)
                {
                    for (int y = 0; y < imageRect.Height; y++)
                    {
                        currentPixel = (y * stride) + x;
                        image1Pixel = image1Raw[currentPixel];
                        image2Pixel = image2Raw[currentPixel];

                        srcRed = (image1Pixel >> 16) & 0xFF;
                        srcGreen = (image1Pixel >> 8) & 0xFF;
                        srcBlue = image1Pixel & 0xFF;

                        dstRed = (image2Pixel >> 16) & 0xFF;
                        dstGreen = (image2Pixel >> 8) & 0xFF;
                        dstBlue = image2Pixel & 0xFF;

                        finRed = (uint)((alphaSrc * (float)srcRed) + 
                            (alphaDst * (float)dstRed));
                        finGreen = (uint)((alphaSrc * (float)srcGreen) + 
                            (alphaDst * (float)dstGreen));
                        finBlue = (uint)((alphaSrc * (float)srcBlue) +
                            (alphaDst * (float)dstBlue));

                        image1Raw[currentPixel] = 
                            (uint)(finBlue | finGreen << 8 | finRed << 16 | 0xFF << 24);
                    }
                }
            }
            image2.UnlockBits(image2Data);
        }
        image1.UnlockBits(image1Data);
    }
}
Share:

Launching a URL in the user's default browser

This has bitten me a few times. If you use Process.Start("url") it will work some of the time but you'll see a "The system cannot find the file specified" Win32Exception on some systems. Bummer.

Lots of people suggest looking up the HTTP handler in HKEY_CLASSES_ROOT but this is flawed as well - on my system for instance HTTP is registered to Firefox even though I'm actually using Chrome and I'd be unhappy waiting half an hour for Firefox to wake up and show the requested web page.

From XP there are a couple of registry settings tied to the current user's preferred browser.

First check HKEY_CURRENT_USER\Software\Clients\StartMenuInternet. This key will only exist if the user has overridden the system default browser - the default value is used to access the details (for me, it's chrome.exe).

If the user hasn't set a default then check HKEY_LOCAL_MACHINE\Software\Clients\StartMenuInternet. The default value here is the system default browser (on my system it's FIREFOX.EXE).

There is a set of subkeys under HKEY_LOCAL_MACHINE\Software\Clients\StartMenuInternet that contain details about each registered browser. The default value from StartMenuInternet (either HKCU or HKLM) is the subkey to look for. The path to my default browser is in this key:

HKEY_LOCAL_MACHINE\Software\Clients\StartMenuInternet\chrome.exe\shell\open\command

Now I can Process.Start(path, url) to safely launch the default browser. You can fall back to Process.Start(url) if registry access fails for some reason, but be prepared for that Win32 exception.

Share:

I Thought He Came With You

Robert Ellison's Blog

7,250,102,861 people still need to read this blog.

Hope for Hulu?

Hope for Hulu?