WPF 单向绑定失效

11

我试图将两个不同的WPF控件绑定到ViewModel中的同一属性,一个是CheckBox.IsChecked,另一个是Expander.IsExpanded。我想要实现的行为是让CheckBox影响ViewModel(因此也会影响Expander),但不要反过来绑定。类似于:

Checkbox Checked -> ViewModel property set to frue -> Expander.Expand
Checkbox Unchecked -> ViewModel property set to false -> Expander.Collapse
Expander Expanded -> Nothing else affected
Expander Collapsed -> Nothing else affected

以下是 XAML 代码:

<Window x:Class="WpfApplication9.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Expander IsExpanded="{Binding IsChecked, Mode=OneWay}">
        <Expander.Header>
            <CheckBox IsChecked="{Binding IsChecked}" Content="Is Checked"/>
        </Expander.Header>
        <TextBlock Text="Expanded!"/>
    </Expander>
</Window>

并且这段代码:

using System.ComponentModel;
using System.Windows;

namespace WpfApplication9
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new ViewModel();
        }
    }

    public class ViewModel: INotifyPropertyChanged
    {
        private bool _isChecked;
        public bool IsChecked
        {
            get { return _isChecked; }
            set
            {
                _isChecked = value;
                NotifyPropertyChange("IsChecked");
            }
        }

        protected void NotifyPropertyChange(string PropertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged = delegate { };
    }
}

现在我的问题是,一旦我点击展开/折叠Expander,绑定似乎就停止工作了。有人能解释一下这是为什么,我怎样才能实现这个功能?先谢谢了!

3个回答

14

新回答

发现您可以通过在扩展器上将UpdateSourceTrigger设置为Explicit来实现此目的。这将保持绑定为双向,但从不更新源,因为您告诉它除非明确告诉它要更新源,否则不要更新源。

<Expander IsExpanded="{Binding IsChecked, UpdateSourceTrigger=Explicit}">
    <Expander.Header>
        <CheckBox IsChecked="{Binding IsChecked}" Content="Is Checked"/>
    </Expander.Header>
    <TextBlock Text="Expanded!"/>
</Expander>

为了让评论有意义,同时也因为我仍然认为将视图特定的代码放在视图的代码后面没有问题,我将保留我的旧答案:


旧答案

个人认为,由于这是视图特定的代码,使用复选框单击事件设置展开器的IsExpanded值是没有问题的。

private void MyCheckBox_Click(object sender, RoutedEventArgs e)
{
    MyExpander.IsExpanded = ((CheckBox)sender).IsChecked.GetValueOrDefault();
}
你可以进一步提高代码的通用性,通过移除名称并导航可视树来查找与复选框关联的展开器。以下是使用我构建的一些可视树帮助程序的示例。
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
    var chk = (CheckBox)sender;
    var expander = VisualTreeHelpers.FindAncestor<Expander>(chk);

    if (expander != null)
        expander.IsExpanded = chk.IsChecked.GetValueOrDefault();
}

我正在创建一个视图,其中至少有6个带有复选框的展开器。这是太多的代码了,我更喜欢不使用任何代码,因为它会降低视图的灵活性。 - Federico Berasategui
如果您有多个Expanders/CheckBoxes,您可能为它们设置了默认样式,并且可以在样式中将Click事件设置为EventSetter。我会使用一些方法来遍历可视树,以查找与该CheckBox相关联的Expander,这样您就不需要使用命名值了。 - Rachel
@HighCore 请看我的更新答案。它可以在没有任何代码后台的情况下运行。 - Rachel
我认为在代码后台使用事件是一个不好的想法。这会导致混乱的代码 - 特别是如果您试图遵循MVVM模式(推荐)。避免导航可视树,因为它可能会导致性能问题,具体取决于树的大小。 - tsells
UpdateSourceTrigger=Explicit 工作得非常完美。有 5 个滑块,一个叫做“(全部)”,然后是 Top、Right、Bottom、Left(用于像 CSS 边框一样的像素大小)。如果更改了“(全部)”的值,它应该始终将其余 4 个值下推,但其他 4 个值不应影响“(全部)” 。谢谢! - Adam Plocher
你太棒了。我遇到了与你相同的问题,当将Expander的IsExpanded属性绑定到ViewModel中一个类的“HasErrors”依赖属性时。双向绑定导致Expander设置HasErrors,而单向绑定导致UI不更新。你的方法强制使得在这种情况下单向绑定正常工作。我欠你一杯啤酒! - Richard Moore

1
如果您希望复选框影响展开器(但反之不行),则正常绑定展开器并在复选框上使用OneWayToSource
<Window x:Class="WpfApplication9.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
   <Expander IsExpanded="{Binding IsChecked}">
      <Expander.Header>
         <CheckBox IsChecked="{Binding IsChecked, Mode=OneWayToSource}" Content="Is Checked"/>
      </Expander.Header>
      <TextBlock Text="Expanded!"/>
   </Expander>
</Window>

在复选框上使用OneWayToSource将允许它:

  • 修改底层属性(因此影响到也绑定到该属性的展开器)
  • 不受其他组件对底层属性所做更改的影响

1
我无法这样做,因为复选框是此处的主要数据表示。它应始终反映ViewModel中的值。 - Federico Berasategui
@HighCore 我本来想点赞的,但是我想到了那个问题,所以觉得我应该等一下。不过如果不需要的话,这是一个很好的解决方案。 - Rachel
1
根据MSDN的说法,复选框的默认模式通常是双向的,但您是否尝试过显式地编写它?"{Binding IsChecked,Mode = TwoWay}" - Renaud Dumont
@RenaudDumont 这只会在展开器展开/折叠时使复选框选中/取消选中,这不是期望的行为。 - Rachel
1
@EsotericScreenName 是的,但是使用此答案中发布的代码,将其添加到复选框中会导致在展开/折叠扩展器时复选框被选中/取消选中,因为扩展器通过双向绑定绑定到相同的值。 - Rachel
@Rachel,不是很清楚,但我认为Renaud打算将该代码应用于问题的绑定,而不是Nate Kohl的答案。将其应用于Nate的答案没有意义,因为他的关键点在于OneWayToSource,而评论则谈论默认绑定状态。您的回答是准确的。无论如何,它都不能解决问题。 - Esoteric Screen Name

1

如果您想避免任何代码后台,您可以在ViewModel中为ExpanderCheckBox状态之间添加一定的分离程度:

            private bool _isChecked;
            public bool IsChecked
            {
                get { return _isChecked; }
                set
                {
                    _isChecked = value;
                    NotifyPropertyChange("IsChecked");
                    IsExpanded = value;
                }
            }

            private bool _isExpanded;
            public bool IsExpanded
            {
                get { return _isExpanded; }
                set
                {
                    _isExpanded = value;
                    NotifyPropertyChange("IsExpanded");
                }
            }

    <Expander IsExpanded="{Binding IsExpanded}">
        <Expander.Header>
            <CheckBox IsChecked="{Binding IsChecked}" Content="Is Checked" x:Name="cb"/>
        </Expander.Header>
        <TextBlock Text="Expanded!"/>
    </Expander>

是的,我把这当作最后的办法。 - Federico Berasategui

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