Search code examples
wpfdata-binding

Binding MenuItem in UserControl to collapse frame in Mainwindow


I'm somewhat new to WPF databinding so bear with me. The behavior I want is to bind a MenuItem in a UserControl and use this to collapse a frame in MainWindow. I'm using and booltoVisibility converter to help with the type conversion. When I run the code the frame doesn't collapse. In the output window I get the error "Cannot save value from target back to source... System Argument exception: "True" is not a valid value for Property CollapseFrame". I think it may have something to do with how I constructed the dependency property but I can't quite figure it out.

MainWindow.xaml

<Window x:Class="TestBooleanToVisibilityConverter.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestBooleanToVisibilityConverter"
    xmlns:local1="clr-namespace:WpfApp1"
    Title="MainWindow"
    SizeToContent="WidthAndHeight" 
    DataContext="{Binding RelativeSource={RelativeSource Self}}">

<Grid Margin="30">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.Resources>
        <local:BoolToVisibleOrHidden x:Key="BoolToVisConverter" Collapse="True" Reverse="True" />
    </Grid.Resources>
    <Frame 
        Grid.Column="0" 
        Grid.Row="0"
        MinWidth= "200"
        MinHeight= "20"
        BorderBrush="Gray"
        BorderThickness="3"
        Visibility="{Binding FrameIsVisible, Converter={StaticResource BoolToVisConverter}}" >
    </Frame>

    <Grid Grid.Row="1">
        <local1:Menu
        CollapseFrame="{Binding FrameIsVisible, Converter={StaticResource BoolToVisConverter}}" >
        </local1:Menu>
    </Grid>

</Grid>

MainWindow.xaml.cs

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using System.Windows;  
using System.Windows.Controls;  
using System.Windows.Data;  
using System.Windows.Documents;  
using System.Windows.Input;  
using System.Windows.Media;  
using System.Windows.Media.Imaging;  
using System.Windows.Navigation;  
using System.Windows.Shapes;  
using WpfApp1;  

namespace TestBooleanToVisibilityConverter
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            FrameIsVisible = false;
        }

        public bool FrameIsVisible { get; set; }
    }
}

Menu.xaml

    <UserControl x:Class="WpfApp1.Menu"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             x:Name="myUserControl"
             mc:Ignorable="d">
    <Menu >
        <MenuItem Header="_File">
            <MenuItem x:Name="HideFrame" Header="Hide Frame" IsCheckable="True" IsChecked="{Binding ElementName=myUserControl, Path=CollapseFrame}"  />
        </MenuItem>
    </Menu>
</UserControl>

Menu.xaml.cs

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

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for Menu.xaml
    /// </summary>
    public partial class Menu : UserControl
    {
        public Menu()
        {
            InitializeComponent();
        }

        public static readonly DependencyProperty CollapseFrameProperty =
            DependencyProperty.Register("CollapseFrame",
            typeof(Visibility),
            typeof(Menu),
            new FrameworkPropertyMetadata(Visibility.Visible));

        public Visibility CollapseFrame
        {
            get { return (Visibility)GetValue(CollapseFrameProperty); }
            set { SetValue(CollapseFrameProperty, value); }
        }
    }
}

BooleanToVisibleOrHidden.cs

    using System;
using System.Windows.Data;
using System.Windows;

namespace TestBooleanToVisibilityConverter {
    class BoolToVisibleOrHidden : IValueConverter
    {
        #region Constructors

        public BoolToVisibleOrHidden() { }
        #endregion

        #region Properties
        public bool Collapse { get; set; }
        public bool Reverse { get; set; }
        #endregion

        #region IValueConverter Members
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            bool bValue = (bool)value;

            if (bValue != Reverse)
            {
                return Visibility.Visible;
            }
            else
            {
                if (Collapse)
                    return Visibility.Collapsed;
                else
                    return Visibility.Hidden;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Visibility visibility = (Visibility)value;

            if (visibility == Visibility.Visible)
                return !Reverse;
            else
                return Reverse;
        }
        #endregion
    }
}

Solution

  • You need a converter that converts from a Visibility to a Boolean:

    public class VisibleOrHiddenToBool : IValueConverter
    {
        #region Properties
        public bool Collapse { get; set; }
        public bool Reverse { get; set; }
        #endregion
    
        #region IValueConverter Members
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Visibility visibility = (Visibility)value;
    
            if (visibility == Visibility.Visible)
                return !Reverse;
            else
                return Reverse;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            bool bValue = (bool)value;
    
            if (bValue != Reverse)
            {
                return Visibility.Visible;
            }
            else
            {
                if (Collapse)
                    return Visibility.Collapsed;
                else
                    return Visibility.Hidden;
            }
        }
        #endregion
    }
    

    Menu.xaml:

    <Grid>
        <Grid.Resources>
            <local:VisibleOrHiddenToBool x:Key="converter" />
        </Grid.Resources>
        <Menu >
            <MenuItem Header="_File">
                <MenuItem x:Name="HideFrame" Header="Hide Frame" IsCheckable="True" 
                              IsChecked="{Binding ElementName=myUserControl, Path=CollapseFrame, Converter={StaticResource converter}}"  />
            </MenuItem>
        </Menu>
    </Grid>
    

    You should then set the binding to the FrameIsVisible property in the window to TwoWay:

    <local:Menu x:Name="menu" CollapseFrame="{Binding FrameIsVisible, Converter={StaticResource BoolToVisConverter}, Mode=TwoWay}" />
    

    Finally, you also need to implement the INotifyPropertyChanged interface and raise PropertyChanged notifications in the window:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        private bool _frameIsVisible;
        public bool FrameIsVisible
        {
            get { return _frameIsVisible; }
            set { _frameIsVisible = value; OnPropertyChanged(); }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }