Fede
Fede

Reputation: 44038

WPF One Way Binding broken

Im trying to bind 2 different WPF controls to the same property in the ViewModel, a CheckBox.IsChecked and an Expander.IsExpanded. The behavior I want to achieve is to have the CheckBox affect the ViewModel (and therefore the Expander as well), but not the other way bound. Something like:

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

Here's the 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>

and the Code:

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 { };
    }
}

Now my problem is, as soon as I click on the Expander to expand / collapse it, the Binding seems to stop working. Can anyone explain to me why this is happening and how do I achieve this? Thanks in advance!

Upvotes: 11

Views: 3481

Answers (3)

Rachel
Rachel

Reputation: 132548

New Answer

Discovered you could do this by setting your UpdateSourceTrigger to Explicit on your Expander. This keeps the binding as Two-Way, but never updates the Source since you're telling it not to update the source unless you explicitly tell it to.

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

Leaving my old answer below so the comments make sense, and because I still feel there is no problem with view-specific code going in the code-behind of a view :)


Old Answer

Personally since this is View-Specific code, I see no problem with using a CheckBox click event to set the Expander's IsExpanded value.

private void MyCheckBox_Click(object sender, RoutedEventArgs e)
{
    MyExpander.IsExpanded = ((CheckBox)sender).IsChecked.GetValueOrDefault();
}

You could make this even more generic by removing the names and navigating the Visual Tree to find the Expander associated with the CheckBox. Here's an example using some Visual Tree Helpers I built

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();
}

Upvotes: 14

Esoteric Screen Name
Esoteric Screen Name

Reputation: 6112

If you'd like to avoid any code-behind, you can add a degree of separation between the Expander and CheckBox states in your ViewModel:

            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>

Upvotes: 1

Nate Kohl
Nate Kohl

Reputation: 35914

If you want the checkbox to affect the expander (but not vice versa) then bind the expander normally and use OneWayToSource on the checkbox:

<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>

Using OneWayToSource on the checkbox will allow it to:

  • modify the underlying property (and therefore affect the expander, which is also bound to that property)
  • not be affected by other components that make changes to the underlying property

Upvotes: 1

Related Questions