WPF Binding and Dynamically Assigning StringFormat Property

This code (inspired from DefaultValueConverter.cs @ referencesource.microsoft.com) works for a two way binding to a TextBox or similar control, as long as the FormatString leaves the ToString() version of the source property in a state that can be converted back. (i.e. format like "#,0.00" is OK because "1,234.56" can be parsed back, but FormatString="Some Prefix Text #,0.00" will convert to "Some Prefix Text 1,234.56" which can't be parsed back.)

XAML:

<TextBox>
    <TextBox.Text>
        <MultiBinding Converter="{StaticResource ToStringFormatConverter}" 
                ValidatesOnDataErrors="True" NotifyOnValidationError="True" TargetNullValue="">
            <Binding Path="Property" TargetNullValue="" />
            <Binding Path="PropertyStringFormat" Mode="OneWay" />
        </MultiBinding>
    </TextBox.Text>
</TextBox>

Note duplicate TargetNullValue if the source property can be null.

C#:

/// <summary>
/// Allow a binding where the StringFormat is also bound to a property (and can vary).
/// </summary>
public class ToStringFormatConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 1)
            return System.Convert.ChangeType(values[0], targetType, culture);
        if (values.Length >= 2 && values[0] is IFormattable)
            return (values[0] as IFormattable).ToString((string)values[1], culture);
        return null;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        var targetType = targetTypes[0];
        var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType);
        if (nullableUnderlyingType != null) {
            if (value == null)
                return new[] { (object)null };
            targetType = nullableUnderlyingType;
        }
        try {
            object parsedValue = ToStringFormatConverter.TryParse(value, targetType, culture);
            return parsedValue != DependencyProperty.UnsetValue
                ? new[] { parsedValue }
                : new[] { System.Convert.ChangeType(value, targetType, culture) };
        } catch {
            return null;
        }
    }

    // Some types have Parse methods that are more successful than their type converters at converting strings
    private static object TryParse(object value, Type targetType, CultureInfo culture)
    {
        object result = DependencyProperty.UnsetValue;
        string stringValue = value as string;

        if (stringValue != null) {
            try {
                MethodInfo mi;
                if (culture != null
                    && (mi = targetType.GetMethod("Parse",
                        BindingFlags.Public | BindingFlags.Static, null,
                        new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider) }, null))
                    != null) {
                    result = mi.Invoke(null, new object[] { stringValue, NumberStyles.Any, culture });
                }
                else if (culture != null
                    && (mi = targetType.GetMethod("Parse",
                        BindingFlags.Public | BindingFlags.Static, null,
                        new[] { typeof(string), typeof(IFormatProvider) }, null))
                    != null) {
                    result = mi.Invoke(null, new object[] { stringValue, culture });
                }
                else if ((mi = targetType.GetMethod("Parse",
                        BindingFlags.Public | BindingFlags.Static, null,
                        new[] { typeof(string) }, null))
                    != null) {
                    result = mi.Invoke(null, new object[] { stringValue });
                }
            } catch (TargetInvocationException) {
            }
        }

        return result;
    }
}

One way may be to create a class that inherits TextBox and in that class create your own dependency property that delegates to StringFormat when set. So instead of using TextBox in your XAML you will use the inherited textbox and set your own dependency property in the binding.


This is a solution from Andrew Olson that uses attached properties and thus can be used in various situations.

Used like this:

<TextBlock 
    local:StringFormatHelper.Format="{Binding FormatString}"
    local:StringFormatHelper.Value="{Binding Value}"
    Text="{Binding (local:StringFormatHelper.FormattedValue)}"
    />

The required helper: (source Gist)

public static class StringFormatHelper
{
    #region Value

    public static DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
        "Value", typeof(object), typeof(StringFormatHelper), new System.Windows.PropertyMetadata(null, OnValueChanged));

    private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        RefreshFormattedValue(obj);
    }

    public static object GetValue(DependencyObject obj)
    {
        return obj.GetValue(ValueProperty);
    }

    public static void SetValue(DependencyObject obj, object newValue)
    {
        obj.SetValue(ValueProperty, newValue);
    }

    #endregion

    #region Format

    public static DependencyProperty FormatProperty = DependencyProperty.RegisterAttached(
        "Format", typeof(string), typeof(StringFormatHelper), new System.Windows.PropertyMetadata(null, OnFormatChanged));

    private static void OnFormatChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        RefreshFormattedValue(obj);
    }

    public static string GetFormat(DependencyObject obj)
    {
        return (string)obj.GetValue(FormatProperty);
    }

    public static void SetFormat(DependencyObject obj, string newFormat)
    {
        obj.SetValue(FormatProperty, newFormat);
    }

    #endregion

    #region FormattedValue

    public static DependencyProperty FormattedValueProperty = DependencyProperty.RegisterAttached(
        "FormattedValue", typeof(string), typeof(StringFormatHelper), new System.Windows.PropertyMetadata(null));

    public static string GetFormattedValue(DependencyObject obj)
    {
        return (string)obj.GetValue(FormattedValueProperty);
    }

    public static void SetFormattedValue(DependencyObject obj, string newFormattedValue)
    {
        obj.SetValue(FormattedValueProperty, newFormattedValue);
    }

    #endregion

    private static void RefreshFormattedValue(DependencyObject obj)
    {
        var value = GetValue(obj);
        var format = GetFormat(obj);

        if (format != null)
        {
            if (!format.StartsWith("{0:"))
            {
                format = String.Format("{{0:{0}}}", format);
            }

            SetFormattedValue(obj, String.Format(format, value));
        }
        else
        {
            SetFormattedValue(obj, value == null ? String.Empty : value.ToString());
        }
    }
}