WPF绑定和动态分配StringFormat属性

15

我有一个表单,该表单基于几个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不是依赖属性。

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

我希望让XAML为我完成工作,这样我就可以避免与代码后台打交道。 我正在使用MVVM模式,并希望尽可能保持视图模型和视图之间的边界清晰。

谢谢!

5个回答

6
这段代码(灵感来自DefaultValueConverter.cs @ referencesource.microsoft.com)可用于实现与TextBox或类似控件的双向绑定,只要FormatString将源属性的ToString()版本保留在可以转换回去的状态中。 (即像“#,0.00”这样的格式是可以的,因为“1,234.56”可以被解析回来,但FormatString =“一些前缀文本#,0.00”将转换为“一些前缀文本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>

如果源属性可以为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;
    }
}

我喜欢它使用开箱即用的WPF,而且解析方法可以轻松地适应你的特定情况。 - Eric

6

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

用法如下:

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

所需的辅助程序:(源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());
        }
    }
}

1
我需要添加“Path”和“RelativeSource”才能使其正常工作。https://dev59.com/X2025IYBdhLWcg3wvIgo#5832247 - YantingChen
请注意,这只能让您使用十进制和十六进制修饰符进行格式化,例如N3或X2。如果您想添加额外的文本,例如“平均温度为{0:N1}摄氏度”,则必须删除所有以if (!format.StartsWith("{0:"))开头的if语句。 - Bjarne
我想问一下,使用这个会不会导致文本无法编辑? - Paul McCarthy
TextBlock的Text属性不再具有可以链接回ViewModel的属性名称。 - undefined

2

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


1
这是一个好建议。我需要研究一下。我有点希望有一种不涉及自定义控件的解决方案,但我当然愿意尝试。我会在一些研究后再回来查看。 - Jason Williams
我正在尝试做同样的事情,但我不确定如何设置附加属性来处理这个问题。我发布了一个新问题:http://stackoverflow.com/q/24119097/65461 - GC.

1
可以创建一个附加的行为,用指定的FormatString替换绑定。如果FormatString依赖属性,则绑定将再次更新。如果绑定被更新,则会将FormatString重新应用于该绑定。
我能想到的唯一两个棘手的问题是:第一个问题是是否要创建两个附加属性,分别与FormatString和目标属性(例如TextBox.Text)协调,以便应用FormatString;或者你可以根据目标控件类型假定正在处理哪个属性。另一个问题可能是,复制现有绑定并稍微修改它可能是不容易的,因为有各种各样的绑定类型,其中可能还包括自定义绑定。
需要考虑的重要问题是,所有这些只实现了从数据到控件的格式化。就我所知,使用MultiBinding和自定义MultiValueConverter来使用原始值和FormatString生成所需输出仍然存在同样的问题,主要是因为ConvertBack方法只提供输出字符串,并且你需要从中解密出FormatString和原始值,而此时几乎总是不可能的。
以下是应该适用于双向格式化和取消格式化的剩余解决方案:
  • 编写一个扩展TextBox的自定义控件,具有所需的格式化行为,就像Jakob Christensen建议的那样。
  • 编写一个从DependencyObject或FrameworkElement派生的自定义值转换器,并在其上具有FormatString DependencyProperty。如果您想使用DependencyObject路线,我相信您可以使用“虚拟分支”技术将值推入FormatString属性中,使用OneWayToSource绑定。另一种更简单的方法可能是继承自FrameworkElement,并将您的值转换器与其他控件一起放置在可视树中,以便在需要时只需通过ElementName进行绑定。
  • 使用类似于我在本帖子顶部提到的附加行为,但不是设置FormatString而是具有两个附加属性,一个用于自定义值转换器,一个用于将传递给值转换器的参数。然后,您将不会修改原始绑定以添加FormatString,而是将转换器和转换器参数添加到绑定中。个人认为,此选项将产生最易读且直观的结果,因为附加行为往往更干净,但仍足够灵活,可用于各种情况,而不仅仅是TextBox。

1
只需将文本框绑定到 MyTextBoxClass 的实例而不是 MyTextBoxClass.Value,并使用值转换器从值和格式字符串创建字符串即可。

另一种解决方案是使用多值转换器,它将绑定到Value和FormatString两个属性。第一种解决方案不支持属性的更改,也就是说,如果Value或FormatString发生更改,值转换器将不会像使用多值转换器并直接绑定到属性时那样被调用。

绑定到 MyTextBoxClass 实例是我尝试过的,但是由于在 ValueConverter 中存在许多属性,在 TextBox 对象上没有地方放置它们,因此 ConvertBack 方法将成为一个问题。所以,我会从 TextBox 得到一个不完整的对象。我会研究多值转换器。然而,FormatString 不可绑定,因为它是一个依赖属性,所以我不确定是否会起作用。 - Jason Williams
这个应该怎么工作?当使用数据绑定更新TextBox时,文本会使用FormatString进行格式化。当用户更新文本框时,他可以输入任何文本,这可能与FormatString的格式不一致。这样可以吗?您确定不想改用掩码文本框吗?另外,FormatString与任何其他公共属性一样可绑定。 - Wallstreet Programmer
"FormatString与任何其他公共属性一样可绑定"。但为什么会出现错误提示:“无法在类型为'Binding'的'StringFormat'属性上设置'Binding'。'Binding'只能设置为DependencyObject的DependencyProperty。” - jpierson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接