Hey guys,

So I'm designing my issue editing form in the WPF version of Comicster, and one of the fields you can edit is the release date of the issue. Thing is, if you don't know (or don't care) what the release date is, you can leave it blank. The property itself is a Nullable DateTime, so it doesn't store any value if you don't supply one.

The problem is that the default binding between a DateTime (even a nullable one) and a TextBox in WPF doesn't allow you to leave the text empty! It wants you to enter a valid DateTime value.

So the first step was to create an IValueConverter which could convert between DateTime and String, but return null if the string is empty. No sweat.

The next problem, as outlined by living legend Douglas Stockwell here, is that any exceptions raised from a custom IValueConverter aren't caught by the built-in ExceptionValidationRule. So if you enter "fred" in my TextBox, the whole program crashes and burns. Not ideal. The only solution Douglas found (and it's a good one IMHO) is to use your IValueConverter as a ValidationRule too.

With that working, I added a few little niceties to the code. So now you can type "today", "now", "yesterday" and "tomorrow" into the TextBox and they'll parse correctly. I'm still considering allowing values like "Monday" and returning the date of the most recent Monday, but I want that to be culture-dependent so I'm not doing it just yet.

Anyway, here's the code! Enjoy!

class NullableDateTimeConverter : ValidationRule, IValueConverter
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (value == null || value.ToString().Trim().Length == 0) return null;

        return new ValidationResult( 
            ConvertBack(value, typeof(DateTime?), null, cultureInfo) != DependencyProperty.UnsetValue,
            "Please enter a valid date, or leave this value blank");
    }

    #region IValueConverter Members
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return "";
        DateTime? dt = value as DateTime?;
        if (dt.HasValue)
        {
            return parameter == null ? dt.Value.ToString() : dt.Value.ToString(parameter.ToString());
        }
        return ""; 
    } 

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || value.ToString().Trim().Length == 0) return null;
        string s = value.ToString();

        if (s.CompareTo("today") == 0) return DateTime.Today;
        if (s.CompareTo("now") == 0) return DateTime.Now;
        if (s.CompareTo("yesterday") == 0) return DateTime.Today.AddDays(-1);
        if (s.CompareTo("tomorrow") == 0) return DateTime.Today.AddDays(1);

        DateTime dt; 
        if (DateTime.TryParse(value.ToString(), out dt)) return dt; 

        return DependencyProperty.UnsetValue; 
    }  
    #endregion
}