简体   繁体   English

WPF 绑定和动态分配 StringFormat 属性

[英]WPF Binding and Dynamically Assigning StringFormat Property

I have a form that is generated based on several DataTemplate elements.我有一个基于几个 DataTemplate 元素生成的表单。 One of the DataTemplate elements creates a TextBox out of a class that looks like this: DataTemplate 元素之一从如下所示的类中创建一个 TextBox:

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

I need a way to "bind" the value in the FormatString property to the "StringFormat" property of the binding.我需要一种方法将 FormatString 属性中的值“绑定”到绑定的“StringFormat”属性。 So far I have:到目前为止,我有:

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

However, since StringFormat isn't a dependency property, I cannot bind to it.但是,由于 StringFormat 不是依赖属性,我无法绑定到它。

My next thought was to create a value converter and pass the FormatString property's value in on the ConverterParameter, but I ran into the same problem -- ConverterParameter isn't a DependencyProperty.我的下一个想法是创建一个值转换器并将 FormatString 属性的值传递给 ConverterParameter,但我遇到了同样的问题——ConverterParameter 不是 DependencyProperty。

So, now I turn to you, SO.所以,现在我转向你,所以。 How do I dynamically set the StringFormat of a binding;如何动态设置绑定的 StringFormat; more specifically, on a TextBox?更具体地说,在 TextBox 上?

I would prefer to let XAML do the work for me so I can avoid playing with code-behind.我更愿意让 XAML 为我完成这项工作,这样我就可以避免使用代码隐藏。 I'm using the MVVM pattern and would like to keep the boundaries between view-model and view as un-blurred as possible.我正在使用 MVVM 模式,并希望尽可能保持视图模型和视图之间的界限不模糊。

Thanks!谢谢!

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.这段代码(灵感来自DefaultValueConverter.cs @ referencesource.microsoft.com )适用于双向绑定到 TextBox 或类似控件,只要 FormatString 将源属性的 ToString() 版本保留在可以转换的状态背部。 (ie 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.) (即像 "#,0.00" 这样的格式是可以的,因为 "1,234.56" 可以解析回来,但 FormatString="Some Prefix Text #,0.00" 将转换为无法解析回来的 "Some Prefix Text 1,234.56"。)

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

C#: 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.一种方法可能是创建一个继承TextBox的类,并在该类中创建您自己的依赖属性,该属性在设置时委托给StringFormat So instead of using TextBox in your XAML you will use the inherited textbox and set your own dependency property in the binding.因此,您将使用继承的文本框并在绑定中设置您自己的依赖项属性,而不是在 XAML 中使用TextBox

This is a solution from Andrew Olson that uses attached properties and thus can be used in various situations.这是来自Andrew Olson的解决方案,它使用附加属性,因此可以在各种情况下使用。

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

One could create an attached behavior that could replace the binding with one that has the FormatString specified.可以创建一种附加行为,该行为可以用指定了 FormatString 的行为来替换绑定。 If the FormatString dependency property then the binding would once again be updated.如果 FormatString 依赖属性,则绑定将再次更新。 If the binding is updated then the FormatString would be reapplied to that binding.如果绑定已更新,则 FormatString 将重新应用于该绑定。

The only two tricky things that I can think that you would have to deal with.我认为您必须处理的唯一两件棘手的事情。 One issue is whether you want to create two attached properties that coordinate with each other for the FormatString and the TargetProperty on which the binding exist that the FormatString should be applied (ex. TextBox.Text) or perhaps you can just assume which property your dealing with depending on the target control type.一个问题是您是否要为 FormatString 和 TargetProperty 创建两个相互协调的附加属性,在该属性上应该应用 FormatString 绑定(例如 TextBox.Text),或者您可以假设您处理的是哪个属性取决于目标控件类型。 The other issue may be that it may be non-trivial to copy an existing binding and modifying it slightly given the various types of bindings out there which might also include custom bindings.另一个问题可能是,考虑到各种类型的绑定(其中可能还包括自定义绑定),复制现有绑定并对其稍作修改可能并非易事。

It's important to consider though that all of this only achieves formatting in the direction from your data to your control.重要的是要考虑到所有这些只能实现从数据到控件的方向的格式化。 As far as I can discover using something like a MultiBinding along with a custom MultiValueConverter to consume both the original value and the FormatString and produce the desired output still suffers from the same problem mainly because the ConvertBack method is only given the output string and you would be expected to decipher both the FormatString and the original value from it which at that point is almost always impossible.据我所知,使用 MultiBinding 之类的东西和自定义 MultiValueConverter 来消耗原始值和 FormatString 并产生所需的输出仍然遇到同样的问题,主要是因为 ConvertBack 方法只给出了输出字符串,你会期望从中破译 FormatString 和原始值,这在那时几乎总是不可能的。

The remaining solutions that should work for bidirectional formatting and unformatting would be the following:适用于双向格式化和取消格式化的其余解决方案如下:

  • Write a custom control that extends TextBox that has the desired formatting behavior like Jakob Christensen suggested.编写一个扩展 TextBox 的自定义控件,该控件具有 Jakob Christensen 建议的所需格式行为。
  • Write a custom value converter that derives from either DependencyObject or FrameworkElement and has a FormatString DependencyProperty on it.编写一个从 DependencyObject 或 FrameworkElement 派生并具有 FormatString DependencyProperty 的自定义值转换器。 If you want to go the DependencyObject route I believe you can push the value into the FormatString property using the OneWayToSource binding with a "virtual branch" technique.如果您想使用 DependencyObject 路线,我相信您可以使用 OneWayToSource 绑定和“虚拟分支”技术将值推送到 FormatString 属性中。 The other easier way may to instead inherit from FrameworkElement and place your value converter into the visual tree along with your other controls so that you can just bind to it when needed by ElementName.另一种更简单的方法可能是从 FrameworkElement 继承并将您的值转换器与其他控件一起放入可视化树中,以便您可以在 ElementName 需要时绑定到它。
  • Use an attached behavior similar to the one I mentioned at the top of this post but instead of setting a FormatString instead have two attached properties, one for a custom value converter and one for the parameter that would be passed to the value converter.使用类似于我在本文开头提到的行为的附加行为,但不是设置 FormatString,而是具有两个附加属性,一个用于自定义值转换器,另一个用于将传递给值转换器的参数。 Then instead of modifying the original binding to add the FormatString you would be adding the converter and the converter parameter to the binding.然后,不是修改原始绑定以添加 FormatString,而是将转换器和转换器参数添加到绑定中。 Personally I think this option would result in the most readable and intuitive result because attached behaviors tend to be more clean yet still flexible enough to use in a variety of situations other than just a TextBox.我个人认为这个选项会产生最易读和最直观的结果,因为附加行为往往更干净,但仍然足够灵活,可以在除 TextBox 之外的各种情况下使用。

Just bind the textbox to the instance of a MyTextBoxClass instead of MyTextBoxClass.Value and use a valueconverter to create a string from the value and formatstring.只需将文本框绑定到 MyTextBoxClass 的实例而不是 MyTextBoxClass.Value 并使用 valueconverter 从值和格式字符串创建一个字符串。

Another solution is to use a multivalue converter which would bind to both Value and FormatString.另一种解决方案是使用一个多值转换器,它可以同时绑定到 Value 和 FormatString。

The first solution don't support changes to properties, that is if value or formatstring changes the value converter will not be called like it would be if you are using a multivalueconverter and binding directly to the properties.第一个解决方案不支持对属性的更改,也就是说,如果值或格式字符串发生更改,则不会像使用多值转换器并直接绑定到属性时那样调用值转换器。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM