Home > Net >  Bind MenuItem.IsEnabled to property of a view model
Bind MenuItem.IsEnabled to property of a view model

Time:12-18

I have an MVVM WPF project with the following code:
MultiplexerVM.cs

public class MultiplexerVM : BaseViewModel
{
    public ObservableCollection<MultiplexVM> Multiplexes { get; set; } = new();
    public MultiplexVM SelectedMultiplex { get; set; }
    public ICommand CheckAll => new CheckBoxCommand(Multiplexes);
}

MultiplexVM.cs

public class MultiplexVM : BaseViewModel
{
    public bool IsChecked { get; set; }
}

MultiplexerV.xaml

<UserControl x:Class="MKVStudio.Views.MultiplexerV"
             xmlns:vm="clr-namespace:MKVStudio.ViewModels"
             xmlns:s="clr-namespace:System;assembly=mscorlib">
    <UserControl.Resources>
        <s:Boolean x:Key="True">True</s:Boolean>
        <s:Boolean x:Key="False">False</s:Boolean>
    </UserControl.Resources>
    <Grid>
        <ListView ItemsSource="{Binding Multiplexes}"
                  SelectedItem="{Binding SelectedMultiplex}">
            <ListView.View>
                <GridView>
                    <GridViewColumn>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox IsChecked="{Binding IsChecked}"Margin="3"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                           ...
                </GridView>
            </ListView.View>                
            <ListView.ContextMenu>
                <ContextMenu>
                    <MenuItem Command="{Binding CheckAll}"
                              CommandParameter="{StaticResource True}">
                        <MenuItem.Header>
                            <TextBlock Text="Check all"/>
                        </MenuItem.Header>
                    </MenuItem>
                    <MenuItem Command="{Binding CheckAll}"
                              CommandParameter="{StaticResource False}">
                        <MenuItem.Header>
                            <TextBlock Text="Uncheck all"/>
                        </MenuItem.Header>
                    </MenuItem>
                </ContextMenu>
            </ListView.ContextMenu>
        </ListView>
    </Grid>
</UserControl>

My goal is to bind IsEnabled of the context menu items to the property IsChecked of MultiplexVM.cs. The idea was to implement an IValueConverter (passing Multiplexes as value and bool as parameter). The converter returns value.Where(m => m.IsChecked == parameter).Count > 0. Essentially, when all Multiplexes are unchecked the menu item Check all is enabled and the menu item Uncheck all is disabled. The reverse thing is happening when all Multiplexes are checked. The problem here is that the converter is invoked only once when it is declared basically, and checking and unchecking the items does not trigger the converter to see what is happening.

I have tried to implement an IMultiValueConverter (but failing to use it correctly) and pass three values like this:

<MenuItem.IsEnabled>
    <MultiBinding>
        <Binding Source="{Binding Multiplexes.Count}" />
        <Binding Source="{Binding Multiplexes}" />
        <Binding Source="{StaticResource True}" /> <!--respectivly False to the other menu item-->
    </MultiBinding>
</MenuItem.IsEnabled>

This doesn't work. I've tried <Binding Path="Multiplexes.Count" /> and <Binding Path="Multiplexes" />, but also doesn't work (the values passed to the converter are Unset).

Is my idea for using MultiBinding even feasible and what am I doing wrong when using it?

CodePudding user response:

Why do you need to bind IsChecked to IsChecked and IsEnabled at once? This is very strange if you look at it from the Single Responsibility Principle. If you are sure that you are doing it right, you can do it like this:

<CheckBox IsChecked="{Binding IsChecked}"
          IsEnabled="{Binding IsEnabled}" />

And make your class look like something like this:

public class MultiplexVM : BaseViewModel
{
    public bool IsChecked 
    { 
        get => isChecked; 
        set
        {
            isChecked = value;
            isEnabled = value;
            RaisePropertyChanged(nameof(IsChecked));
            RaisePropertyChanged(nameof(IsEnabled));
        }; 
    }

    private bool isChecked;

    public bool IsEnabled
    { 
        get => isEnabled; 
        set
        {
            isChecked = value;
            isEnabled = value;
            RaisePropertyChanged(nameof(IsChecked));
            RaisePropertyChanged(nameof(IsEnabled));
        }; 
    }

    private bool isChecked;
}

CodePudding user response:

From what I understand, you want to make an object bound to a "parent" (MenuItem => MultiplexerVM) be dependant on a property of its child collection (CheckBox => MultiplexVM.IsChecked, which is an item in MultiplexerVM.Multiplexes)

In this scenario, a child has to be somehow aware of its parent (when the child changes, it has to "push" the change up to the parent; in other words, the parent has to be informed when the change happens).

I can think of two ways to do it:

  • on the VM level: in every MultiplexVM, set a reference to the parent view model or collection, then you can update the CanCheckAll / CanUncheckAll functionality (however you implement it) every time the child's IsChecked changes (tedious; I suppose you can also do this with events, but attaching PropertyChanged handler to every child item is also a bit much)

  • cheat a bit by using the GUI level: you can update the CanCheckAll / CanUncheckAll functionality whenever the CheckBox is clicked

Below is an example of how you can implement the 2nd version.

In your MultiplexerVM:

public bool CanCheckAll => Multiplexes.Any(a => !a.IsChecked);
public bool CanUncheckAll => Multiplexes.Any(a => a.IsChecked);

public void RefreshCheckUncheckAll()
{
    NotifyPropertyChanged(nameof(CanCheckAll));
    NotifyPropertyChanged(nameof(CanUncheckAll));
}

Then, call RefreshCheckUncheckAll() in CheckAll command implementation and in:

private void CheckBox_Click(object sender, RoutedEventArgs e)
{
    ((MultiplexerVM)this.DataContext).RefreshCheckUncheckAll();
}

Then, the xaml will look something like this:

<ListView ItemsSource="{Binding Multiplexes}" SelectedItem="{Binding SelectedMultiplex}">
        <ListView.ContextMenu>
            <ContextMenu>
                <MenuItem
                    Command="{Binding CheckAll}"
                    CommandParameter="{StaticResource True}"
                    IsEnabled="{Binding CanCheck}">
                    <MenuItem.Header>
                        <TextBlock Text="Check all" />
                    </MenuItem.Header>
                </MenuItem>
                <MenuItem
                    Command="{Binding CheckAll}"
                    CommandParameter="{StaticResource False}"
                    IsEnabled="{Binding CanUncheck}">
                    <MenuItem.Header>
                        <TextBlock Text="Uncheck all" />
                    </MenuItem.Header>
                </MenuItem>
            </ContextMenu>
        </ListView.ContextMenu>
        <ListView.View>
            <GridView>
                <GridViewColumn>
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Margin="3" Text="{Binding Name}" />
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn>
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <CheckBox Margin="3" IsChecked="{Binding IsChecked}" Click="CheckBox_Click" />
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </GridView>
        </ListView.View>

    </ListView>
  • Related