WPF Binding and Dynamically Assigning StringFormat Property WPF Binding and Dynamically Assigning StringFormat Property wpf wpf

WPF Binding and Dynamically Assigning StringFormat Property


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


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.