9
votes

I'm using MVVM, VS 2008, and .NET 3.5 SP1. I have a list of items, each exposing an IsSelected property. I have added a CheckBox to manage the selection/de-selection of all the items in the list (updating each item's IsSelected property). Everything is working except the IsChecked property is not being updated in the view when the PropertyChanged event fires for the CheckBox's bound control.

<CheckBox
  Command="{Binding SelectAllCommand}"
  IsChecked="{Binding Path=AreAllSelected, Mode=OneWay}"
  Content="Select/deselect all identified duplicates"
  IsThreeState="True" />

My VM:

public class MainViewModel : BaseViewModel
{
  public MainViewModel(ListViewModel listVM)
  {
    ListVM = listVM;
    ListVM.PropertyChanged += OnListVmChanged;
  }

  public ListViewModel ListVM { get; private set; }
  public ICommand SelectAllCommand { get { return ListVM.SelectAllCommand; } }

  public bool? AreAllSelected
  {
    get
    {
      if (ListVM == null)
        return false;

      return ListVM.AreAllSelected;
    }
  }

  private void OnListVmChanged(object sender, PropertyChangedEventArgs e)
  {
    if (e.PropertyName == "AreAllSelected")
      OnPropertyChanged("AreAllSelected");
  }
}

I'm not showing the implementation of SelectAllCommand or individual item selection here, but it doesn't seem to be relevant. When the user selects a single item in the list (or clicks the problem CheckBox to select/de-select all items), I have verified that the OnPropertyChanged("AreAllSelected") line of code executes, and tracing in the debugger, can see the PropertyChanged event is subscribed to and does fire as expected. But the AreAllSelected property's get is only executed once - when the view is actually rendered. Visual Studio's Output window does not report any data binding errors, so from what I can tell, the CheckBox's IsSelected property is properly bound.

If I replace the CheckBox with a Button:

<Button Content="{Binding SelectAllText}" Command="{Binding SelectAllCommand}"/>

and update the VM:

...

public string SelectAllText
{
  get
  {
    var msg = "Select All";
    if (ListVM != null && ListVM.AreAllSelected != null && ListVM.AreAllSelected.Value)
      msg = "Deselect All";

    return msg;
  }
}

...

private void OnListVmChanged(object sender, PropertyChangedEventArgs e)
{
  if (e.PropertyName == "AreAllSelected")
    OnPropertyChanged("SelectAllText");
}

everything works as expected - the button's text is updated as all items are selected/desected. Is there something I'm missing about the Binding on the CheckBox's IsSelected property?

Thanks for any help!

1

1 Answers

8
votes

I found the problem. It seems a bug existed in WPF 3.0 with OneWay bindings on IsChecked causing the binding to be removed. Thanks to this post for the assistance, it sounds like the bug was fixed in WPF 4.0

To reproduce, create a new WPF project.

Add a FooViewModel.cs:

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace Foo
{
  public class FooViewModel : INotifyPropertyChanged
  {
    private bool? _isCheckedState = true;

    public FooViewModel()
    {
      ChangeStateCommand = new MyCmd(ChangeState);
    }

    public bool? IsCheckedState
    {
      get { return _isCheckedState; }
    }

    public ICommand ChangeStateCommand { get; private set; }

    private void ChangeState()
    {
      switch (_isCheckedState)
      {
        case null:
          _isCheckedState = true;
          break;
        default:
          _isCheckedState = null;
          break;
      }

      OnPropertyChanged("IsCheckedState");
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
      var changed = PropertyChanged;
      if (changed != null)
        changed(this, new PropertyChangedEventArgs(propertyName));
    }
  }

  public class MyCmd : ICommand
  {
    private readonly Action _execute;
    public event EventHandler CanExecuteChanged;

    public MyCmd(Action execute)
    {
      _execute = execute;
    }

    public void Execute(object parameter)
    {
      _execute();
    }

    public bool CanExecute(object parameter)
    {
      return true;
    }
  }
}

Modify Window1.xaml.cs:

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

namespace Foo
{
  public partial class Window1
  {
    public Window1()
    {
      InitializeComponent();
    }

    private void OnClick(object sender, RoutedEventArgs e)
    {
      var bindingExpression = MyCheckBox.GetBindingExpression(ToggleButton.IsCheckedProperty);
      if (bindingExpression == null)
        MessageBox.Show("IsChecked property is not bound!");
    }
  }
}

Modify Window1.xaml:

<Window
  x:Class="Foo.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vm="clr-namespace:Foo"
  Title="Window1"
  Height="200"
  Width="200"
  >

  <Window.DataContext>
    <vm:FooViewModel />
  </Window.DataContext>

  <StackPanel>
    <CheckBox
      x:Name="MyCheckBox"
      Command="{Binding ChangeStateCommand}"
      IsChecked="{Binding Path=IsCheckedState, Mode=OneWay}"
      Content="Foo"
      IsThreeState="True"
      Click="OnClick"/>
    <Button Command="{Binding ChangeStateCommand}" Click="OnClick" Content="Change State"/>
  </StackPanel>
</Window>

Click on the button a few times and see the CheckBox's state toggle between true and null (not false). But click on the CheckBox and you will see that the Binding is removed from the IsChecked property.

The workaround:

Update the IsChecked binding to be TwoWay and set its UpdateSourceTrigger to be explicit:

IsChecked="{Binding Path=IsCheckedState, Mode=TwoWay, UpdateSourceTrigger=Explicit}"

and update the bound property so it's no longer read-only:

public bool? IsCheckedState
{
  get { return _isCheckedState; }
  set { }
}