更改绑定属性为什么在运行时不更新复选框的可视状态?

huangapple go评论97阅读模式
英文:

Why doesn't changing the bound property update the checkbox's visual state during runtime?

问题

我有一个简单的WPF应用程序,其中包含一个下拉框和一个复选框:

XAML如下所示:

<StackPanel>
   <ComboBox ItemsSource="{Binding FileTypes}" SelectedItem="{Binding SelectedFileType, Mode=TwoWay}"/>
   <CheckBox Content="test" IsChecked="{Binding IsChecked}"/>
</StackPanel>

我想要在从下拉框中选择“Word”时强制复选框取消选中状态。我尝试通过在我的数据上下文中实现INotifyPropertyChanged,然后订阅表示下拉框中选定项的属性SelectedFileTypePropertyChanged事件来实现这一点。以下是我到目前为止编写的代码:

public class MainViewModel : INotifyPropertyChanged
{
   private string m_selectedFileType;
   public List<string> FileTypes { get; set; }
   public bool IsChecked { get; set; }

   public string SelectedFileType
   {
      get => m_selectedFileType;
      set
      {
         m_selectedFileType = value;
         PropertyChanged(this, new PropertyChangedEventArgs((nameof(SelectedFileType))));
      }
   }

   public event PropertyChangedEventHandler PropertyChanged;

   public MainViewModel()
   {

      FileTypes = new List<string>();
      FileTypes.Add("Excel");
      FileTypes.Add("Word");

      PropertyChanged += FileTypeChanged;
      SelectedFileType = "Excel";

      IsChecked = true;
   }

   private void FileTypeChanged(object sender, PropertyChangedEventArgs e)
   {
      if (SelectedFileType == "Word")
      {
         IsChecked = false;
      }
   }
}

不幸的是,当我运行应用程序并从下拉框中切换从“Excel”到“Word”时,复选框保持不变,所以我觉得我漏掉了什么。

我已经尝试过切换到TwoWay绑定并使用UpdateSourceTrigger,但这些方法都没有奏效。

为什么在运行时修改SelectedFileType属性不会影响复选框的可视状态,即使我在XAML中明确将其设置为双向绑定呢?

编辑:相关的文档提到了以下内容:

“要检测源更改(适用于OneWay和TwoWay绑定),源必须实现适当的属性更改通知机制,例如INotifyPropertyChanged。”

我开始觉得也许TwoWay绑定和INotifyPropertyChanged有一定的关联。

英文:

I have a simple WPF application with a drop down and a checkbox:

更改绑定属性为什么在运行时不更新复选框的可视状态?

The XAML looks like the following:

&lt;StackPanel&gt;
   &lt;ComboBox ItemsSource=&quot;{Binding FileTypes}&quot; SelectedItem=&quot;{Binding SelectedFileType, Mode=TwoWay}&quot;/&gt;
   &lt;CheckBox Content=&quot;test&quot; IsChecked=&quot;{Binding IsChecked}&quot;/&gt;
&lt;/StackPanel&gt;

I would like to force the checkbox to go unchecked when I select "Word" from the drop down. I've tried to achieve this by implementing INotifyPropertyChanged in my data context and then subscribing to the PropertyChanged event of the property which represents the selected item from the drop down, SelectedFileType. Here's the code I have so far:

public class MainViewModel : INotifyPropertyChanged
{
   private string m_selectedFileType;
   public List&lt;string&gt; FileTypes { get; set; }
   public bool IsChecked { get; set; }

   public string SelectedFileType
   {
      get =&gt; m_selectedFileType;
      set
      {
         m_selectedFileType = value;
         PropertyChanged(this, new PropertyChangedEventArgs((nameof(SelectedFileType))));
      }
   }

   public event PropertyChangedEventHandler PropertyChanged;

   public MainViewModel()
   {

      FileTypes = new List&lt;string&gt;();
      FileTypes.Add(&quot;Excel&quot;);
      FileTypes.Add(&quot;Word&quot;);

      PropertyChanged += FileTypeChanged;
      SelectedFileType = &quot;Excel&quot;;

      IsChecked = true;
   }

   private void FileTypeChanged(object sender, PropertyChangedEventArgs e)
   {
      if (SelectedFileType == &quot;Word&quot;)
      {
         IsChecked = false;
      }
   }
}

Unfortunately, when I run the app and switch from "Excel" to "Word" in the drop down, the check box remains unchanged, so I feel that I am missing something.

I have already tried switching to TwoWay binding and using UpdateSourceTrigger but none of those things have worked.

Why does modifying the SelectedFileType property during runtime not affect the visual state of the checkbox, even though I have clearly set it as a two-way binding in XAML?

EDIT

The relevant documentation does mention the following:

> To detect source changes (applicable to OneWay and TwoWay bindings), the source must implement a suitable property change notification mechanism such as INotifyPropertyChanged.

I'm starting to think that perhaps TwoWay binding and INotifyPropertyChanged sort of go hand in hand...

答案1

得分: 1

您的 IsChecked 属性未引发 INotifyPropertyChanged.PropertyChanged 事件。

每当您希望从源属性(例如 IsChecked)发送数据到目标属性(例如 CheckBox.IsChecked)时,必须引发 INotifyPropertyChanged.PropertyChanged 事件。否则,绑定将不知道它必须更新目标。

BindingMode.TwoWay 仅表示数据可以回传到源。此模式还包括 BindingMode.OneWay 行为以使其双向。换句话说,当绑定目标(例如 CheckBox)更改双向绑定的值时,目标必须引发更改通知(通过依赖属性)以通知绑定它必须更新绑定源。仍然,源必须引发更改通知,以便在更新值时触发绑定(更准确地说是 BindingExpression)。

绑定是被动的,不会主动在源和目标之间传递数据。发送者(单向绑定中的绑定源和双向绑定中的绑定源和绑定目标,或单向到源模式中的绑定目标)必须始终通过引发更改通知来显式触发绑定以传输数据(通过实现 INotifyPropertyChanged 并引发 PropertyChanged,或通过将属性实现为依赖属性)。

请参阅Microsoft Docs:数据绑定概述(WPF .NET)

此外,您当前的代码将在任何属性更改时都更新 IsChecked 属性。您必须在 PropertyChanged 事件处理程序中筛选正确的属性:

private void FileTypeChanged(object sender, PropertyChangedEventArgs e)
{
  switch (e.PropertyName)
  {
    case nameof(this.IsChecked):
      if (this.SelectedFileType == "Word")
      {
        this.IsChecked = false;
      }
      break;
  }
}

但因为您是这些属性的所有者,所以不必处理 PropertyChanged 事件。而是从属性设置器中调用例如 OnIsChekedChanged 方法。您可以在.NET文档中找到更好和推荐的 INotifyPropertyChanged 实现:PropertyChanged 示例

改进和修复版本的代码可能如下所示:

class MainViewModel : INotifyPropertyChanged
{
  private bool isChecked;
  public bool IsChecked
  {
    get => this.isChecked;
    set
    {
      this.isChecked = value;
      OnPropertyChanged();
      OnIsCheckedChanged();
    }
  }

  private string selectedFileType;
  public string SelectedFileType
  {
    get => this.selectedFileType;
    set
    {
      bool oldValue = this.IsChecked;
      this.selectedFileType = value;
      OnPropertyChanged();
      OnIsCheckedChanged(oldValue, value);
    }
  }

  protected virtual void OnIsCheckedChanged(bool oldValue, bool newValue)
  {
    if (this.SelectedFileType.Equals("Word", StringComparison.InvariantCultureIgnoreCase))
    {
      this.IsChecked = false;
    }
  }
}
英文:

Your IsCheckedproperty doesn't raise the INotifyPropertyChanged.PropertyChanged event.

Whenever you want to send data from the source property (e.g. IsChecked) to a target property (e.g. CheckBox.IsChecked), you must raise the INotifyPropertyChanged.PropertyChanged. Otherwise the Binding won't know that it has to update the target.

BindingMode.TwoWay only means that data can be send back to the source. This mode also includes BindingMode.OneWay behavior to make it two way. In other words, when the the binding target (e.g. CheckBox) changes the value of a two way binding the target has to raise a change notification (via dependency property) to notify the Binding that it has to update the binding source.
Still the source has to raise a change notification in case it updates the value to trigger the Binding (it's actually the BindingExpression to be more precise).

The binding is passive in that it doesn't actively send data between source and target. The sender (binding source in OneWay and binding source and binding target in TwoWay or binding target in OneWyToSource mode) must always explicitly trigger the binding to transfer data by raising a change notification (by implementing INotifyPropertyChangedand raisePropertyChanged` or by implementing the property as dependency property).

See Microsoft Docs: Data binding overview (WPF .NET).

Furthermore, your current code will update the IsChecked property every time that any property changes. You must filter for the correct property in your PropertyChanegd event handler:

private void FileTypeChanged(object sender, PropertyChangedEventArgs e)
{
  switch (e.PropertyName)
  {
    case nameof(this.IsChecked):
      if (this.SelectedFileType == &quot;Word&quot;)
      {
        this.IsChecked = false;
      }
      break;
  }
}

But because you are the the owner of those properties, you don't have to handle the PropertyChanged event. Instead invoke a e.g. OnIsChekedChanged method from the property setter.
You can find a better and recommended INotifyPropertyChanged implementation in the .NET docs: PropertyChanged example.

The improved and fixed version of your code could look as follows:

class MainViewModel : INotifyProeprtyChanged
{
  private bool isChecked;
  public bool IsChecked
  {
    get =&gt; this.isChecked;
    set
    {
      this.isChecked = value;
      OnPropertyChanged();
      OnIsCheckedChanged();
    }
  }

  private string selectedFileType;
  public string SelectedFileType
  {
    get =&gt; this.selectedFileType;
    set
    {
      bool oldValue = this.IsChecked;
      this.selectedFileType = value;
      OnPropertyChanged();
      OnIsCheckedChanged(oldValue, value);
    }
  }

  protected virtual void OnIsCheckedChanged(bool oldValue, bool newValue)
  {
    if (this.SelectedFileType.Equals(&quot;Word&quot;, StringComparison.InvariantCultureIgnoreCase)
    {
      this.IsChecked = false;
    }
  }
}

答案2

得分: 0

你忘了通知视图 IsChecked 属性已更改。

我稍微修改了您的代码,这应该可以工作:

这是XAML代码:

<StackPanel>
   <ComboBox ItemsSource="{Binding FileTypes}" SelectedItem="{Binding SelectedFileType}"/>
   <CheckBox Content="test" IsChecked="{Binding IsChecked}"/>
</StackPanel>

这是更改将发生的地方:

public class MainViewModel : INotifyPropertyChanged
{ 
    public MainViewModel()
    {
        SelectedFileType = "Excel";
        IsChecked = true;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    // 实现一个方法来引发属性更改事件
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // 创建一个新的私有字段,并使用属性公开它,以保持代码一致性
    // 还使用了ObservableCollection,因为它在向集合添加项时会通知视图
    private ObservableCollection<string> file_types = new ObservableCollection<string>() { "Word", "Excel" };
    public ObservableCollection<string> FileTypes
    {
        get => file_types;
        private set => file_types = value; // 通常不需要重新创建ObservableCollection,只需要修改它
    }

    private bool is_checked = false;
    public bool IsChecked
    {
        get => is_checked;
        set
        {
            is_checked = value;
            // 通知视图更改
            OnPropertyChanged(nameof(IsChecked));
        }
    }

    private string m_selectedFileType;
    public string SelectedFileType
    {
        get => m_selectedFileType;
        set
        {
            m_selectedFileType = value;
            OnPropertyChanged(nameof(SelectedFileType));

            // 使用现有属性的绑定,而不是事件
            if (SelectedFileType == "Word")
            {
                // 这将触发属性设置器,从而触发OnPropertyChanged方法
                IsChecked = false; 
            }
        }
    }
}

编辑:我认为您可能误解了Binding Mode的实际工作原理。我发现这篇文章很容易理解。

简而言之;
将任何Mode添加到绑定中要求元素具有共同的绑定属性。例如,一个Slider和一个TextBoxSlider的值绑定到一个名为SliderValue(示例)的属性,而TextBoxText属性绑定到相同的SliderValue属性。Binding Mode只是让我们控制哪个控件可以更改另一个控件的视图值。请注意,它们都具有它们绑定到的共同属性,而在您的情况下并非如此。

考虑到这个代码作为我们的上下文:

<Slider Name="Slider1" Value="0" SmallChange="1" Maximum="100"></Slider>

<TextBox Name="txtValue" Text="{Binding Mode=TwoWay, ElementName=Slider1,Path=Value, UpdateSourceTrigger=PropertyChanged}"></TextBox>

OneWay - 这将允许滑块更改文本框的值,但反之亦然
OneWayToSource - 这将允许文本框更改滑块的值,但反之亦然
TwoWay - 这将允许两个组件互相更新

您的Binding Mode实现不符合这些规则。

英文:

You forgot to notify the view that the IsChecked property has changed.

I took the freedom to modify your code a bit, this should work:

This would be the XAML

&lt;StackPanel&gt;
   &lt;ComboBox ItemsSource=&quot;{Binding FileTypes}&quot; SelectedItem=&quot;{Binding SelectedFileType}&quot;/&gt;
   &lt;CheckBox Content=&quot;test&quot; IsChecked=&quot;{Binding IsChecked}&quot;/&gt;
&lt;/StackPanel&gt;

and this is where the change will happen:

public class MainViewModel : INotifyPropertyChanged
{ 
    public MainViewModel()
    {
        SelectedFileType = &quot;Excel&quot;;
        IsChecked = true;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    // Implement a method to raise property changed calls
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // Created a new private field and exposed it using a property for code consistency
    // Also used ObservableCollection because it notifies the view when an item is added to the collection
    private ObservableCollection&lt;string&gt; file_types = new ObservableCollection&lt;string&gt;() { &quot;Word&quot;, &quot;Excel&quot; };
    public ObservableCollection&lt;string&gt; FileTypes
    {
        get =&gt; file_types;
        private set =&gt; file_types = value; // You usually don&#39;t need to recreate an ObservableCollection, only modify it
    }

    private bool is_checked = false;
    public bool IsChecked
    {
        get =&gt; is_checked;
        set
        {
            is_checked = value;
            // Notifiy the view of the change
            OnPropertyChanged(nameof(IsChecked));
        }
    }

    private string m_selectedFileType;
    public string SelectedFileType
    {
        get =&gt; m_selectedFileType;
        set
        {
            m_selectedFileType = value;
            OnPropertyChanged(nameof(SelectedFileType));

            // Use the existing binding to the property instead of an event
            if (SelectedFileType == &quot;Word&quot;)
            {
                // this will trigger the property setter thus triggering the OnPropertyChanged method
                IsChecked = false; 
            }
        }
    }
}

Edit: I think you have misunderstood how Binding Mode actually works. I found this article to be pretty easy to understand.

TLDR;
Adding any Mode to a binding requires that the elements have a common binding property. For example a Slider and a TextBox. The Slider has its value bound to a property called SliderValue(example) and the TextBox has its Text property bound to the same SliderValue property. The Binding Mode just gives us control over which of these control can change the view value of the other. Notice that they both have a common property they are bound to, which in your case is not true.

Considering this code as our context:

&lt;Slider Name=&quot;Slider1&quot; Value=&quot;0&quot; SmallChange=&quot;1&quot; Maximum=&quot;100&quot; &gt;&lt;/Slider&gt;

&lt;TextBox Name=&quot;txtValue&quot; Text=&quot;{Binding Mode=TwoWay, ElementName=Slider1,Path=Value, UpdateSourceTrigger=PropertyChanged}&quot;&gt;&lt;/TextBox&gt;

OneWay - this will allow the slider to change the value of the text box but not the other way around
OneWayToSource - this will allow the text box to change the value of the slider but not the other way around
Two way - this will allow both components to update each other

Your implementation of a Binding Mode follows none of this.

huangapple
  • 本文由 发表于 2023年6月19日 19:29:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76506185.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定