Kris' Blog

RSS feed

The operation completed successfully.

The stacktrace says it all.

System.Runtime.InteropServices.COMException (0x80070000): The operation completed successfully. (Exception from HRESULT: 0x80070000)
   at MS.Internal.HRESULT.Check(Int32 hr)
   at System.Windows.Media.SafeProfileHandle.ReleaseHandle()
   at System.Runtime.InteropServices.SafeHandle.InternalFinalize()
   at System.Runtime.InteropServices.SafeHandle.Dispose(Boolean disposing)
   at System.Runtime.InteropServices.SafeHandle.Finalize()

We made this go away by calling GC.WaitForPendingFinalizers() but this can cause deadlocks in WPF since some Finalize() methods use Dispatcher.Invoke().


Validating fullwidth alphanumeric input in .Net

One of the things that attracted me to playing Final Fantasy XI (a MMORPG) was the opportunity to play on the same server with Japanese players. I’ve been a fan of anime since I was a kid and always curious about Japanese culture.

I noticed that when a Japanese player says 5000 gil (Final Fantasy currency) it looks wide and spaced funny like “5000 gil.” As Japanese players are generally considered to be more l33t by the American players, some even try to mimic this by adding spaces like so “5 0 0 0 g i l.” People even hack the game to input Japanese text on the American client.

So when my brother called me in a bind about a missed requirement to accept double byte credit card numbers, I knew exactly what he was talking about.
Luckily the web browser deals with the input and on the ASP.NET side it is decoded into a Unicode string. From there it is actually quite simple.

No, int.Parse(string) does not work.

You could loop over each character in the string and use char.IsNumber(char) for validation. You’d probably want to be more specific char.GetUnicodeCategory(char) since we don’t need to support roman numerals.

protected void CustomValidator1_ServerValidate(object source, ServerValidateEventArgs args)
{
    foreach (char c in args.Value)
        if (char.GetUnicodeCategory(c) != UnicodeCategory.DecimalDigitNumber)
            args.IsValid = false;
}

You probably want to normalize it before saving it into the database. You could use (int)char.GetNumericValue(char) but even easier is to use string.Normalize(NormalizationForm.FormKD). You could even normalize the string before validation and run your existing validation on the normalized string.

Output from the Immediate Window (Ctrl-Alt-I)

'\xFF15'
65301 '5'
Char.IsNumber('5')
true
Char.GetUnicodeCategory('5')
DecimalDigitNumber
Char.GetNumericValue('5')
5.0
"5000 gil".Normalize(System.Text.NormalizationForm.FormKD)
"5000 gil"

Visibility.Visible != IsVisible

Today, Robert and I spent a while trying to figure out why we couldn’t give focus to a TextBox we added dynamically. Calling textBox.Focus() was always returning false, yet we could click on it to focus.

Reading the WPF focus overview, Focus() returns false if either IsEnabled, IsVisible or Focusable are false — well it defaults to being enabled, visible and focusable so why isn’t it working?

IsVisible is calculated during the Layout pass, which isn’t going to happen on this dispatcher operation. During this time, textBox.Visibility will return Visibility.Visible and textBox.IsVisible will return false.

Determination of the IsVisible value takes all factors of layout into account. In contrast, Visibility, which is a settable property, only indicates the intention to programmatically make an element visible or invisible.

textBox = new TextBox();
AddChild(textBox);
// textBox.IsVisible == false
textBox.UpdateLayout();
// textBox.IsVisible == true
textBox.Focus() // returns true

In general, read-only properties, especially dependency properties registered as read-only, will need an UpdateLayout (I wouldn’t do that a lot) or you will need to BeginInvoke your work on the Dispatcher to wait for layout.


Adding a Custom Publisher to CruiseControl.NET

Now you too can have a funny lookin’ bunny announce whether you broke the build with your last check-in.

The sample code is a Nabaztag publisher for CC.NET which sends the build status as a text to speech message via the Nabaztag API demonstrating a configurable CruiseControl.NET publisher plugin.

NabaztagPublisher.cs

using System;
using System.Net;
using System.Text;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Core.Util;
using ThoughtWorks.CruiseControl.Remote;

namespace NabaztagCruiseControlPlugin
{
    [ReflectorType("nabaztag")]
    public class NabaztagPublisher: ITask
    {
        private const int SuccessEarPosition = 0;
        private const int FailureEarPosition = 9;
        private const string NabaztagUrlFormat =
@"http://api.nabaztag.com/vl/FR/api.jsp?key={0}&sn={1}&token={2}&posleft={3}&posright={3}&ears=ok&voice=graham22s&tts={4}";

        private readonly WebClient _client;

        private string _key;
        private string _token;
        private string _serialNumber;
        private string _pronounceableName;

        public NabaztagPublisher()
        {
            _client = new WebClient();
            _client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(WebClientDownloadStringCompleted);
        }

        [ReflectorProperty("key")]
        public string Key
        {
            get { return _key; }
            set { _key = value; }
        }

        [ReflectorProperty("token")]
        public string Token
        {
            get { return _token; }
            set { _token = value; }
        }

        [ReflectorProperty("serialNumber")]
        public string SerialNumber
        {
            get { return _serialNumber; }
            set { _serialNumber = value; }
        }

        [ReflectorProperty("pronounceableName", Required=false)]
        public string PronounceableName
        {
            get { return _pronounceableName; }
            set { _pronounceableName = value; }
        }

        public void Run(IIntegrationResult result)
        {
            string name = PronounceableName ?? result.ProjectName;
            string message;
            int earPosition;
            switch (result.Status)
            {
                case IntegrationStatus.Success:
                    earPosition = SuccessEarPosition;
                    if (result.LastIntegrationStatus == IntegrationStatus.Success)
                        message = string.Format(
                            "Yet another successful build for {0}.", name);
                    else
                        message = string.Format(
                            "Recent check ins have fixed the build for {0}.", name);
                    break;
                case IntegrationStatus.Failure:
                    earPosition = FailureEarPosition;
                    if (result.LastIntegrationStatus == IntegrationStatus.Success)
                        message = string.Format(
                            "The build has failed for {0}. Oh dear.", name);
                    else
                        message = string.Format(
                            "The build is still broken for {0} people! Get your act together. Deary me.", name);
                    break;
                case IntegrationStatus.Exception:
                    earPosition = FailureEarPosition;
                    message = string.Format(
                        "Oh dear. The build has failed due to an exception for project {0}. This sucks.", name);
                    break;
                case IntegrationStatus.Unknown:
                default:
                    earPosition = FailureEarPosition;
                    message = string.Format(
                        "Oh dear. The build has failed due to an unknown reason for project {0}. This sucks.", name);
                    break;
            }

            Uri uri = new Uri(
                string.Format(NabaztagUrlFormat, Key, SerialNumber, Token, earPosition, Uri.EscapeDataString(message)));
            _client.DownloadStringAsync(uri, uri);
        }

        void WebClientDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            Log.Debug(string.Format("Nabaztag API: {0}", e.UserState));
            if (e.Error != null)
                Log.Error(e.Error);
            else
                Log.Debug(e.Result);
        }
    }
}

I had trouble finding documentation on CC.NET plugins but I found documentation on adding a custom builder plugin and most the info applies to plugins in general.

I had to name the assembly ccnet.nabaztag.plugin.dll (CC.NET expects plugin assembly files to be in the form ccnet.*.plugin.dll) and mark it with NetReflector (part of the CC.NET distribution) attributes that correspond to the XML tags and attributes used to configure the plugin in the ccnet.config file.

<project>
...
<publishers>
...
<nabaztag key="111111111" serialNumber="111111111" token="111111111"
pronounceableName="My Project" />
</publishers>
</project>

Another thing to note, CC.NET’s static Log util is backed by log4net which is thread safe (in case you were wondering if it was ok for me to log on the async callback, the only drawback is in the log file it will use the thread id and not the name of the project).

Bunny
Edit:
I wanted to keep the example simple and focused on adding a custom publisher to CC.NET but it seems the bunny is more interesting so I wanted to point out this C# Nabaztag API wrapper which is also where I got the “oh deary me” messages from. And yet another library to control your bunny.

Also, for the original CruiseControl.