在自定义控件上使用StringFormat绑定

5

我正在尝试在WPF应用程序中使用自定义控件,但是在使用StringFormat绑定时遇到了一些问题。

问题很容易重现。首先,让我们创建一个名为“TemplateBindingTest”的WPF应用程序。在其中添加一个自定义ViewModel,该ViewModel只有一个属性(Text),并将其分配给Window的DataContext。将Text属性设置为“Hello World!”。

现在,向解决方案中添加一个自定义控件。这个自定义控件非常简单:

using System.Windows;
using System.Windows.Controls;

namespace TemplateBindingTest
{
    public class CustomControl : Control
    {
        static CustomControl()
        {
            TextProperty = DependencyProperty.Register(
                "Text",
                typeof(object),
                typeof(CustomControl),
                new FrameworkPropertyMetadata(null));

            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl), new FrameworkPropertyMetadata(typeof(CustomControl)));
        }

        public static DependencyProperty TextProperty;

        public object Text
        {
            get
            {
                return this.GetValue(TextProperty);
            }

            set
            {
                SetValue(TextProperty, value);
            }
        }
    }
}

当向解决方案中添加自定义控件时,Visual Studio会自动创建一个主题文件夹,并在其中创建一个generic.xaml文件。让我们将控件的默认样式放入其中:
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TemplateBindingTest">

    <Style TargetType="{x:Type local:CustomControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CustomControl}">
                    <TextBlock Text="{TemplateBinding Text}" />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

现在,只需将控件添加到窗口中,并使用StringFormat在Text属性上设置绑定。还可以添加一个简单的TextBlock以确保绑定语法正确:
<Window x:Class="TemplateBindingTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:TemplateBindingTest="clr-namespace:TemplateBindingTest" Title="MainWindow" Height="350" Width="525">
<StackPanel>
    <TemplateBindingTest:CustomControl Text="{Binding Path=Text, StringFormat=Test1: {0}}"/>
    <TextBlock Text="{Binding Path=Text, StringFormat=Test2: {0}}" />
</StackPanel>

编译,运行,然后...窗口上显示的文本是:

你好世界!

测试2:你好世界!

在自定义控件中,StringFormat被完全忽略了。在VS输出窗口上看不到任何错误信息。发生了什么?

编辑:解决方法。

好的,TemplateBinding是误导性的。我找到了原因和一个临时解决方法。

首先,请注意Button的Content属性也存在同样的问题:

<Button Content="{Binding Path=Text, StringFormat=Test3: {0}}" />

那么,发生了什么?让我们使用Reflector并深入到BindingBase类的StringFormat属性。"分析"功能显示该属性被内部DetermineEffectiveStringFormat方法使用。让我们看看这个方法:

internal void DetermineEffectiveStringFormat()
{
    Type propertyType = this.TargetProperty.PropertyType;
    if (propertyType == typeof(string))
    {
         // Do some checks then assign the _effectiveStringFormat field
    }
}

问题就在这里。解析绑定时使用的是effectiveStringFormat字段。而且只有当DependencyProperty的类型为String时才会分配此字段(我的是,就像Button的Content属性一样,是Object)。
为什么是Object?因为我的自定义控件比我粘贴的控件更复杂,就像Button一样,我希望控件的用户能够提供子控件而不仅仅是文本。
那么现在怎么办?我们遇到了甚至存在于WPF核心控件中的行为,所以我可以将其保持“原样”。尽管如此,由于我的自定义控件只在内部项目中使用,并且我希望它在XAML中更容易使用,我决定使用这个hack:
using System.Windows;
using System.Windows.Controls;

namespace TemplateBindingTest
{
    public class CustomControl : Control
    {
        static CustomControl()
        {
            TextProperty = DependencyProperty.Register(
                "Text",
                typeof(string),
                typeof(CustomControl),
                new FrameworkPropertyMetadata(null, Callback));

            HeaderProperty = DependencyProperty.Register(
                "Header",
                typeof(object),
                typeof(CustomControl),
                new FrameworkPropertyMetadata(null));

            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl), new FrameworkPropertyMetadata(typeof(CustomControl)));
        }

        static void Callback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            obj.SetValue(HeaderProperty, e.NewValue);
        }

        public static DependencyProperty TextProperty;
        public static DependencyProperty HeaderProperty;

        public object Header
        {
            get
            {
                return this.GetValue(HeaderProperty);
            }

            set
            {
                SetValue(HeaderProperty, value);
            }
        }

        public string Text
        {
            set
            {
                SetValue(TextProperty, value);
            }
        }
    }
}
Header是我模板绑定中使用的属性。当给Text提供一个值时,由于该属性是String类型,所以会应用StringFormat,然后使用回调将该值转发到Header属性。它能工作,但实现方式很不规范:
  • HeaderText属性不同步,因为当我更新Header时,Text不会被更新。我选择不为Text属性提供getter,以避免一些错误,但如果有人直接从DependencyProperty(GetValue(TextProperty))读取值,仍然可能出现错误。
  • 如果有人同时向HeaderText属性提供值,则可能会发生不可预测的行为,其中一个值将丢失。

总的来说,我不建议使用这种方法。只有在您真正掌控项目时才使用它。如果控件甚至有一点可能在另一个项目中使用,请放弃使用StringFormat

2个回答

6

3

当绑定到一个 string 属性时,使用 StringFormat,而你的控件中的 Text 属性是类型为 object,因此会忽略 StringFormat


1
不正确。运行时在格式化字符串之前调用所有对象的 object.ToString() 方法。 - Tomislav Markovski
2
是的,但在应用StringFormat之前,它会检查依赖属性的基础类型。如果我将我的dp类型更改为String而不是Object,则可以正常工作。 - Kevin Gosse

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