C# 和 wpf - ListBox-Label-ComboBox 之间(单向模式)链绑定的意外行为

C# & wpf - Unexpected behavior of (OneWay-Mode) chain-binding between ListBox-Label-ComboBox

我有以下奇怪的(对我来说)情况
ListBox 绑定(作为源)到具有单向模式的标签,即 ListBox 是只读的。 然后使用 TwoWay 绑定将 Label 绑定到 ComboBox

ListBox --> Label <--> ComboBox - arrows denote binding mode

奇怪的是,当程序启动并且用户通过 ListBox 中的列表进行选择时,所有 3 个控件都按预期运行。 但是一旦从 Combobox 中选择了一个索引,Label 继续正常工作(由 Combo 更新),但是绑定到 ListBox 的 OneWay 消失(为 null)并且 ListBox 无法再更新 Label。

在我看来,当通过 OneWay 绑定之外的其他方式设置标签内容时(如此处的 Combo 更新或可能使用 ValueConverter),此绑定将被 WPF 清除。

另一个奇怪的行为是,如果将 ListBox 和 Label 之间的这种单向绑定转换为双向绑定,那么一切都会完美无缺。

问题是我做错了什么,或者如果这是正常行为,我在哪里可以找到相关文档。

请在下面找到简化代码和 XAML 演示案例。 我的解决方法是使用 ListBox_SelectionChanged.

中的代码设置标签内容
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace Test_Chained_controls
{
   public partial class MainWindow : Window
   {
      public class ComboItems
      {
         public int iDX { get; set; }
         public string sDesc { get; set; }

         public ComboItems(int a, string b)
         {
            iDX = a;
            sDesc = b;
         }
      }

      public class ListItems
      {
         public int iLDX { get; set; }
         public ListItems(int a)
         {
            iLDX = a;
         }
      }

      public List<ListItems> intList = new List<ListItems>();
      public List<ComboItems> idx_StrList = new List<ComboItems>();

      public MainWindow()
      {
         InitializeComponent();

         intList.Add(new ListItems(0));
         intList.Add(new ListItems(1));
         intList.Add(new ListItems(2));
         intList.Add(new ListItems(3));

         idx_StrList.Add(new ComboItems(0, "Zero"));
         idx_StrList.Add(new ComboItems(1, "One"));
         idx_StrList.Add(new ComboItems(2, "Two"));
         idx_StrList.Add(new ComboItems(3, "Three"));
      }

      private void Window_Loaded(object sender, RoutedEventArgs e)
      {
         listBox.ItemsSource = intList;
         comboBox.ItemsSource = idx_StrList;
      }

      private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
      {
         //// Set Label Content in case of OneWay
         // var binding = BindingOperations.GetBinding(label, Label.ContentProperty);
         // if (binding != null)
         // {
         //    if (binding.Mode == BindingMode.OneWay)
         //       {}  // Binding set - do nothing
         // }
         // else label.Content = listBox.SelectedItem;
      }
   }
}

XAML

<Window ... normal stuff
        xmlns:local="clr-namespace:Test_Chained_controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="182" Width="500" Loaded="Window_Loaded">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="140"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="100"/>
        </Grid.RowDefinitions>

        <Label Content="ListBox"    Grid.Row="0" Grid.Column="0" Margin="20,10,0,0" />
        <Label Content="Label"      Grid.Row="0" Grid.Column="1" Margin="20,10,0,0" />
        <Label Content="ComboBox"   Grid.Row="0" Grid.Column="2" Margin="20,10,0,0" />

        <ListBox x:Name="listBox"   Grid.Row="1" Grid.Column="0" Margin="0"  
                 DisplayMemberPath="iLDX" 
                 SelectedIndex="0"
                 IsSynchronizedWithCurrentItem="True" 
                 SelectionChanged="ListBox_SelectionChanged"/>

        <Border BorderThickness="1" Grid.Row="1" Grid.Column="1" Height="30" 
                Margin="20,20,0,0" BorderBrush="#FFACACAC"  >

            <!-- *********** Label with Mode=OneWay or TwoWay *********** -->
            <Label x:Name="label" Width="100" Height="25"
                   Content="{Binding ElementName=listBox, 
                             Path=SelectedItem.iLDX, Mode=OneWay }" />
        </Border>

        <ComboBox x:Name="comboBox" Grid.Row="1" Grid.Column="2" 
                                   Height="30" Margin="20,20,0,0"  

                  DisplayMemberPath="sDesc" 
                  SelectedValue="{Binding ElementName=label, Path=Content, 
                  TargetNullValue=0, FallbackValue=0, Mode=TwoWay}"
                  SelectedValuePath="iDX"  />
    </Grid>
</Window>

编辑

相关文档:Dependency properties overview

Local value: A local value might be set through the convenience of the property wrapper, which also equates to setting as an attribute or property element in XAML, or by a call to the SetValue method using a property of a specific instance. If you set a local value by using a binding or a static resource, these each act in the precedence as if a local value was set, and bindings or resource references are erased if a new local value is set.

再往下

If you set another local value for a property that originally held a Binding value, you will overwrite the binding entirely, not just the binding's run-time value.

据我了解,存在与此案例相关的某种错误,已通过引入 DependencyObject 得到修复。SetCurrentValue The Control Local Values Bug Solution

public void SetCurrentValue (System.Windows.DependencyProperty dp, object value);
// Sets the value of a dependency property without changing its value source.

在我看来,Combobox TwoWay 绑定仍在使用 SetValue,这就是为什么在使用我的 (combobox) 时删除了 (label) 的绑定。

为了克服这个问题,我将(comboBox)的TwoWay绑定改为OneWay,并在comboBox_DropDownClosed事件中输入以下内容(显示当前选中的Item),以便通过代码更新(label)不删除现有绑定

  private void comboBox_DropDownClosed(object sender, System.EventArgs e)
  {
     Binding binding = BindingOperations.GetBinding(label, Label.ContentProperty);
     if (binding != null)
     {
        ComboItems ComboItem = comboBox.SelectedItem as ComboItems;
        int iDX = ComboItem.iDX;

        // Set label value without affecting existing binding
        label.SetCurrentValue(Label.ContentProperty, iDX);
     }
  }

通过使用 SetCurrentValue,代码现在可以按照 "simulating" TwoWay 模式的最初预期运行。

没什么奇怪的。数据绑定旨在以这种方式工作。当您将绑定分配给依赖项 属性 时,这意味着您将此依赖项 属性 的本地值更改为绑定表达式。绑定源提供的任何更新都将是此依赖项的有效值属性。如果绑定以单向模式工作,则从其他绑定源对此依赖项 属性 的任何更新都将覆盖本地值,从而导致绑定丢失。另一方面,因为两种模式假设更新绑定源,依赖对象会将任何非表达式值计为有效值,绑定将一直有效,直到您替换或清除它。

  • DependencyObject.GetValue获取有效值。
  • DependencyObject.ReadLocalValue 获取本地值。
  • DependencyObject.SetValue 设置本地值。
  • DependencyObject.SetCurrentValue设置有效值。
  • DependencyObject.ClearValue 清除本地值。