【问题标题】:WPF Binding and Dynamically Assigning StringFormat PropertyWPF 绑定和动态分配 StringFormat 属性
【发布时间】: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 模式,并希望尽可能保持视图模型和视图之间的界限不模糊。

谢谢!

【问题讨论】:

    标签: wpf dynamic binding string-formatting


    【解决方案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”才能使其工作。 stackoverflow.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。

    【讨论】:

    • 这是个好建议。我得调查一下。我有点希望有一个不涉及自定义控件的解决方案,但我当然愿意接受。我会在稍作研究后回来查看。
    • 我正在尝试做同样的事情,但我不确定如何设置附加属性来处理这个问题。我发布了一个新问题:stackoverflow.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 之外的各种情况下使用。

    【讨论】:

      猜你喜欢
      • 2016-01-06
      • 2018-05-10
      • 2018-03-12
      • 1970-01-01
      • 2011-08-17
      • 2017-08-25
      • 1970-01-01
      • 2011-03-11
      • 1970-01-01
      相关资源
      最近更新 更多