为什么在绑定中添加一个无操作转换器会改变其行为?

7

我正在测试一个自己构建的用户控件,但是遇到了一个对我来说无法解释的问题。

这个控件是ComboBox的扩展,用于处理特定自定义类型的值。它有一个依赖属性,用于绑定该自定义类型的目标属性。

我在setter中添加了追踪语句,可以看到该属性已被设置。但它并没有出现在我的用户控件中。

通常情况下,我会说,好吧,我的用户控件里肯定有bug。尽管我感到困惑,但我可能确实存在这样的问题。但是,这个问题不是关于查找我的控件中的错误。请继续阅读;接下来的部分变得奇怪起来。

我还使用了Bea Stollnitz的小值转换器来帮助调试绑定:

public class DebuggingConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value; // Add the breakpoint here!!
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException("This method should never be called");
    }
}

这个想法是我将这个转换器添加到我的绑定中,然后设置断点以查看推送到目标的值。好的,这很有效。我可以看到该值正在被推出。
事实上,它有点太有效了。如果将DebuggingConverter附加到绑定上,用户控件会显示该值。如果没有,则不会显示。
这怎么可能?一个什么都不做的值转换器如何影响绑定控件的行为?
编辑:
虽然这不太可能有帮助,但以下是用户控件的XAML:
<a:CodeLookupBox
    Grid.Column="1"
    Grid.IsSharedSizeScope="True"
    MinWidth="100"
    Style="{Binding Style}">
    <a:CodeLookupBox.CodeLookupTable>
        <Binding Path="Codes" Mode="OneWay"/>
    </a:CodeLookupBox.CodeLookupTable>
    <a:CodeLookupBox.SelectedCode>
        <Binding Path="Value" Mode="TwoWay" ValidatesOnDataErrors="True"/>
    </a:CodeLookupBox.SelectedCode>
</a:CodeLookupBox>

没有第二个绑定上的转换器,控件的行为就像我没有设置“SelectedCode”一样。即使在“OnSelectedCodePropertyChanged”处理程序中的跟踪语句显示“e.Value”确实包含正确的值。无论是否附加了转换器,都会发生这种情况。
我一直在尝试通过思维实验来反向工程化解决此问题:如果您想创建一个绑定的用户控件,其行为会因为附加了一个空操作转换器而改变,您该如何做?我对绑定不了解足以提出答案。

1
这非常有趣。你能贴一些 XAML 在那里展示绑定吗? - Steve Danner
这是一个好问题。我没有答案,但我知道Bea网站上的帖子提供了一些关于调试绑定的建议。当您不使用转换器时是否抛出任何异常? - Joel B Fant
抱歉我不能帮你,但是我会点赞的,因为DebuggingConverter是个很酷的想法(即使这不是你的想法)。 - Benny Jobigan
1
如果将第二个绑定更改为OneWay,它是否有效? - Gabe
你应该提供一个更详细的示例。你的DataContext对象是什么样子的?SelectedCode DP是如何定义的? - Eli Arbel
显示剩余2条评论
4个回答

2

好消息是,我知道为什么在不使用值转换器时SelectedCode没有被设置。坏消息是,我仍然有点神秘,但问题已经被提升到更高的层次,并且我有了一个解决方法。

这个控件本质上是一个强类型组合框,具有一堆额外的功能,这些功能由于它知道其中包含哪些项目而变得可能。 SelectedCodeCodeLookupTable属性是强类型的,它们隐藏了底层的SelectedItemItemsSource属性,后者不是强类型的。(顺便说一下,这就是为什么这是一个用户控件而不是ComboBox的子类;我不希望这些属性可见,因为如果它们被错误地设置,会发生很多事情,但都不是好事。)

这里是正在发生的事情。当附加值转换器时,这是我的调试输出(数字是控件的哈希码,因为我有一堆同时绘制的控件当程序初始化时):

14626603: OnCodeLookupTablePropertyChanged
   CodeLookupTable property set to Proceedings.Model.CodeLookupTable
   box.MainComboBox.ItemsSource = MS.Internal.Data.EnumerableCollectionView
14626603: OnSelectedCodePropertyChanged:
   SelectedCode property set to Unlicensed Driver [VC12500(A)]
   box.MainComboBox.ItemsSource = MS.Internal.Data.EnumerableCollectionView

这是预期的行为。设置了CodeLookupTable属性,因此将SelectedCode设置为该集合中的一项会正确地在底层ComboBox上设置SelectedItem。但是如果没有值转换器,我们会得到这样的结果:
16143157: OnSelectedCodePropertyChanged:
   SelectedCode property set to Unlicensed Driver [VC12500(A)]
   box.MainComboBox.ItemsSource = 
16143157: OnCodeLookupTablePropertyChanged
   CodeLookupTable property set to Proceedings.Model.CodeLookupTable
   box.MainComboBox.ItemsSource = MS.Internal.Data.EnumerableCollectionView

在这里,SelectedCode属性在CodeLookupTable属性之前被设置。因此,当方法尝试在底层的ComboBox上设置SelectedItem时,什么也不会发生,因为ItemsSource为空。
这就是问题的根源。我愚蠢地假设绑定更新其目标的顺序与它们在XAML中声明的顺序相同。(我将绑定表示为元素而不是属性的原因之一是因为XML文档中元素的顺序是确定的,而属性的顺序不是。这并不意味着我没有考虑过这个问题。)显然情况并非如此。
我还假设(可能稍微不那么愚蠢),绑定更新其目标的顺序不取决于它们是否具有附加值转换器。事实并非如此。我想知道它还依赖于什么。
幸运的是,我有一种解决方法。由于我的CodeLookup对象包含对CodeLookupTable的引用,如果还没有设置,我可以使SelectedCode setter首先设置CodeLookupTable(从而设置ItemsSource属性)。这将消除这个问题,而无需在绑定上添加虚拟值转换器,并希望绑定行为永远不会改变。 编辑 这里是属性声明的样子:
#region SelectedCode

public static readonly DependencyProperty SelectedCodeProperty = DependencyProperty.Register(
    "SelectedCode", typeof(CodeLookup), typeof(CodeLookupBox),
    new FrameworkPropertyMetadata(OnSelectedCodePropertyChanged));

private static void OnSelectedCodePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    CodeLookupBox box = (CodeLookupBox)source;
    CodeLookup code = e.NewValue as CodeLookup;
    // this right here is the fix to the original problem:
    if (box.CodeLookupTable == null && code != null)
    {
        box.CodeLookupTable = code.Table;
    }
    box.MainComboBox.SelectedItem = e.NewValue;
}

public CodeLookup SelectedCode
{
    get { return GetValue(SelectedCodeProperty) as CodeLookup; }
    set { SetValue(SelectedCodeProperty, value); }
}

#endregion

#region CodeLookupTable

public static readonly DependencyProperty CodeLookupTableProperty = DependencyProperty.Register(
    "CodeLookupTable", typeof(CodeLookupTable), typeof(CodeLookupBox),
    new FrameworkPropertyMetadata(OnCodeLookupTablePropertyChanged));

private static void OnCodeLookupTablePropertyChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
    CodeLookupBox box = (CodeLookupBox)source;
    CodeLookupTable table = (CodeLookupTable)e.NewValue;

    box.ViewSource = new CollectionViewSource { Source = table.Codes };
    box.View = box.ViewSource.View;
    box.MainComboBox.ItemsSource = box.View;

}

public CodeLookupTable CodeLookupTable
{
    get { return GetValue(CodeLookupTableProperty) as CodeLookupTable; }
    set { SetValue(CodeLookupTableProperty, value); }
}

#endregion

有趣。我假设SelectedCodeCodeLookup是DPs。你能展示一下它们的声明以及如何将更改传递给ItemsSourceSelectedItem吗? - Joel B Fant

0

尝试实现ConvertBack函数,因为您正在使用双向绑定,所以问题可能是异常将被“xaml”忽略,但在调试模式下,您可能会停止“发送值回来”的操作?


一个好主意,如此之好以至于我已经实现了它。但是它没有改变任何东西。我将ConvertBack更改为返回“value”,并获得了相同的结果。此外,我不必通过调试器逐步执行才能使转换器发挥作用。我可以运行发布版本。 - Robert Rossney

0

嗯...非常奇怪。我曾经在处理具有多个转换器的MultiBindings时看到过类似的行为,但没有在简单绑定上看到过。出于好奇,您是如何在控件上定义您的DP的?(包括回调、元数据选项等)

尝试一些东西(在删除转换器钩子之后):

  • 默认模式(即删除TwoWay)
  • 删除ValidatesOnDataErrors
  • 向SelectedCode DP添加AffectsRender

0

设置SelectedCode属性有什么作用?你能发布一下属性更改处理程序的代码吗?

你建议调试显示属性被设置为正确的值,因此最明显的建议是提供预期行为的代码不正确。


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