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 theCanCheckAll / CanUncheckAll
functionality (however you implement it) every time the child'sIsChecked
changes (tedious; I suppose you can also do this with events, but attachingPropertyChanged
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 theCheckBox
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>