WPF绑定和动态分配StringFormat属性

时间:2010-06-23 05:08:49

标签: wpf dynamic binding string-formatting

我有一个基于多个DataTemplate元素生成的表单。其中一个DataTemplate元素从类中创建一个TextBox:

public class MyTextBoxClass
{
   public object Value { get;set;}
   //other properties left out for brevity's sake
   public string FormatString { get;set;}
}

我需要一种方法将FormatString属性中的值“绑定”到绑定的“StringFormat”属性。到目前为止,我有:

<DataTemplate DataType="{x:Type vm:MyTextBoxClass}">
 <TextBox Text="{Binding Path=Value, StringFormat={Binding Path=FormatString}" />
</DataTemplate>

但是,由于StringFormat不是依赖项属性,因此无法绑定它。

我的下一个想法是创建一个值转换器并在ConverterParameter上传递FormatString属性的值,但我遇到了同样的问题 - ConverterParameter不是DependencyProperty。

所以,现在我转向你,所以。如何动态设置绑定的StringFormat;更具体地说,在TextBox上?

我更愿意让XAML为我工作,这样我就可以避免使用代码隐藏。我正在使用MVVM模式,并且希望尽可能不模糊视图模型和视图之间的界限。

谢谢!

5 个答案:

答案 0 :(得分:2)

一种方法可能是创建一个继承TextBox的类,并在该类中创建自己的依赖项属性,该属性在设置时委托给StringFormat。因此,不要在XAML中使用TextBox,而是使用继承的文本框并在绑定中设置自己的依赖项属性。

答案 1 :(得分:2)

此代码(灵感来自DefaultValueConverter.cs @ referencesource.microsoft.com)适用于对TextBox或类似控件的双向绑定,只要FormatString将Source属性的ToString()版本保留在可以转换回的状态中。 (例如“#,0.00”之类的格式是可以的,因为“1,234.56”可以被解析回来,但FormatString =“Some Prefix Text#,0.00”将转换为“Some Prefix Text 1,234.56”,无法解析。)< / p>

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>

如果source属性可以为null,请注意重复的TargetNullValue。

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

答案 2 :(得分:1)

只需将文本框绑定到MyTextBoxClass的实例而不是MyTextBoxClass.Value,并使用valueconverter从value和formatstring创建一个字符串。

另一个解决方案是使用一个多值转换器,它将绑定到Value和FormatString。

第一个解决方案不支持对属性的更改,即如果value或formatstring更改,则不会调用值转换器,就像使用多值转换器并直接绑定到属性一样。

答案 3 :(得分:1)

可以创建一个附加行为,可以用指定了FormatString的绑定替换绑定。如果FormatString依赖属性然后再次更新绑定。如果绑定已更新,则FormatString将重新应用于该绑定。

我认为你必须处理的两件棘手的事情。一个问题是你是否要为FormatString和TargetProperty创建两个相互协调的附加属性,其中存在应该应用FormatString的绑定(例如TextBox.Text)或者你可以只假设你的交易属性取决于目标控件类型。另一个问题可能是复制现有绑定并稍微修改它可能是非常重要的,因为那里可能还包括自定义绑定的各种类型的绑定。

重要的是要考虑所有这些只能实现从数据到控件的方向格式化。至于我发现使用类似MultiBinding和自定义MultiValueConverter之类的东西来消耗原始值和FormatString并产生所需的输出仍然会遇到同样的问题,主要是因为ConvertBack方法只给出输出字符串而你会期望从它解密FormatString和原始值,在那时几乎总是不可能。

应该用于双向格式化和取消格式化的其余解决方案如下:

  • 编写一个自定义控件,扩展具​​有所需格式行为的TextBox,如Jakob Christensen建议。
  • 编写一个自定义值转换器,该转换器派生自DependencyObject或FrameworkElement,并在其上具有FormatString DependencyProperty。如果你想去DependencyObject路由,我相信你可以使用OneWayToSource绑定和“虚拟分支”技术将值推送到FormatString属性。另一种更简单的方法是继承FrameworkElement并将值转换器与其他控件一起放入可视树中,以便在ElementName需要时可以绑定它。
  • 使用与我在本文顶部提到的类似的附加行为,但不是设置FormatString而是具有两个附加属性,一个用于自定义值转换器,另一个用于将传递给值转换器的参数。然后,不是修改原始绑定以添加FormatString,而是将转换器和转换器参数添加到绑定。我个人认为这个选项会产生最可读和最直观的结果,因为附加的行为往往更干净,但仍然足够灵活,可以在除TextBox之外的各种情况下使用。

答案 4 :(得分:0)

这是来自Andrew Olson的解决方案,它使用附加属性,因此可以在各种情况下使用。

像这样使用:

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

所需的帮助者:(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());
        }
    }
}