WPF 绑定和动态分配 StringFormat 属性

Posted

技术标签:

【中文标题】WPF 绑定和动态分配 StringFormat 属性【英文标题】:WPF Binding and Dynamically Assigning StringFormat Property 【发布时间】:2011-03-07 03:45:53 【问题描述】:

我有一个基于多个 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 不是依赖属性,我无法绑定到它。

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

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

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

谢谢!

【问题讨论】:

【参考方案1】:

这是来自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());
        
    

【讨论】:

我需要添加“Path”和“RelativeSource”才能使其工作。 ***.com/a/5832247/417939 请注意,这只会让您使用 N3 或 X2 等十进制和十六进制修饰符进行格式化。如果您想添加额外的文本,例如“平均温度为 0:N1 摄氏度”,您必须删除所有以 if (!format.StartsWith("0:")) 开头的 if 语句 我是否认为使用它不允许编辑文本?【参考方案2】:

此代码(受DefaultValueConverter.cs @ referencesource.microsoft.com 启发)适用于与 TextBox 或类似控件的双向绑定,只要 FormatString 使源属性的 ToString() 版本处于可转换回的状态。 (即像 "#,0.00" 这样的格式是可以的,因为 "1,234.56" 可以被解析回来,但是 FormatString="Some Prefix Text #,0.00" 将转换为 "Some Prefix Text 1,234.56" 不能被解析回来。)

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>

如果源属性可以为空,请注意重复的 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;
    

【讨论】:

我喜欢它使用开箱即用的 WPF,并且解析方法可以很容易地适应您的具体情况。【参考方案3】:

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

【讨论】:

这是个好建议。我得调查一下。我有点希望有一个不涉及自定义控件的解决方案,但我当然愿意接受。我会在稍作研究后回来查看。 我正在尝试做同样的事情,但我不确定如何设置附加属性来处理这个问题。我发布了一个新问题:***.com/q/24119097/65461【参考方案4】:

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

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

第一个解决方案不支持更改属性,也就是说,如果值或格式字符串发生更改,则不会像使用多值转换器并直接绑定到属性时那样调用值转换器。

【讨论】:

绑定到 MyTextBoxClass 实例是我尝试过的,但是 ValueConverter 中的 ConvertBack 方法将成为一个问题,因为我在 TextBox 上没有很多属性目的。所以,我会从 TextBox 返回一个不完整的对象。我将研究多值转换器。但是,FormatString 是不可绑定的,因为它是一个依赖属性,所以我不确定它是否会起作用。 这应该如何工作?当使用数据绑定更新 TextBox 时,使用 FormatString 格式化文本。当用户更新文本框时,他可以输入任何可能与 FormatString 格式不一致的文本。那样可以么?您确定不想使用蒙面文本框吗?此外,FormatString 与任何其他公共属性一样可绑定。 “FormatString 与任何其他公共属性一样可绑定”解释为什么您会收到一条错误消息,指出“无法在 'Binding' 类型的 'StringFormat' 属性上设置 'Binding'。只能在 DependencyObject 的 DependencyProperty 上设置“绑定”。”【参考方案5】:

可以创建一个附加行为,该行为可以将绑定替换为指定了 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 之外的各种情况下使用。

【讨论】:

以上是关于WPF 绑定和动态分配 StringFormat 属性的主要内容,如果未能解决你的问题,请参考以下文章

WPF StringFormat 格式化文本

WPF绑定StringFormat不会改变输出[重复]

WPF StringFormat 格式化文本

wpf Content数据绑定StringFormat起作用的原理和解决

WPF 与 StringFormat 的绑定在 ToolTips 上不起作用

WPF 绑定 StringFormat 短日期字符串