Bind MenuItem.IsEnabled to property of a view model - c#

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?

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

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

WPF CheckBox Command inside a Listview DataTemplate

My CheckBox Command does not work. I want to pass the SelectedItem of the ListView to the Command when the SelectedItem is checked or unchecked, but the Command does not execute at all. I also suspect my CommandParameter is not configured correctly?
I am pretty sure the problem is because the CheckBox is within a ListView DataTemplate.
Can someone show me how to set this up? I tried to follow examples I found, but nothing seems to work. thanks.
XAML
<ListView x:Name="lvReferralSource" ItemsSource="{Binding ReferralSourceTypeObsCollection}" Style="{StaticResource TypeListViewStyle}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<CheckBox x:Name="ckbReferralIsChecked" Content="{Binding Value}" IsChecked="{Binding Active}" Style="{StaticResource CheckBoxStyleBase2}"
Command="{Binding CheckBoxIsChecked}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}, Path=SelectedItem}">
</CheckBox>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
CODE
private ICommand _CheckBoxIsChecked;
public ICommand CheckBoxIsChecked
{
get
{
if (_CheckBoxIsChecked == null)
{
_CheckBoxIsChecked = new RelayCommand<object>(ExecuteCheckBoxIsChecked, CanExecuteCheckBoxIsChecked);
}
return _CheckBoxIsChecked;
}
}
public bool CanExecuteCheckBoxIsChecked(object parameter)
{
return true;
}
public void ExecuteCheckBoxIsChecked(object parameter)
{
Mouse.OverrideCursor = Cursors.Wait;
if (parameter != null)
{
//Do Stuff...
}
Mouse.OverrideCursor = Cursors.Hand;
}
Your command should get executed provided that the CheckBoxIsChecked property belongs to the data object where the Value and Active properties are defined.
If it belongs to the view model, you could bind to it using a RelativeSource:
<CheckBox x:Name="ckbReferralIsChecked" Content="{Binding Value}" IsChecked="{Binding Active}"
Style="{StaticResource CheckBoxStyleBase2}"
Command="{Binding DataContext.CheckBoxIsChecked, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}">

Biding contextmenu menuitem visibility with a list

I have a ContextMenu attached to a ListBox who offer two options : create and delete element. I want to hide only the "Delete" element if the ListBox data is empty.
I've tried to bind the property "Visibility" with a variable in the view's code setting it to "Collapsed" or "Visible", but it didn't work.
XAML :
<ListBox ItemsSource="{Binding ElementList}"
SelectedItem="{Binding SelectedElement}"
SelectionChanged="ListBoxProjects_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Name="Add" Click="Add_Click" Header="Add element" />
<MenuItem Name="Delete" Click="Delete_Click"
HeaderStringFormat="Delete element {0}"
Header="{Binding SelectedElement.Name}"
Visibility="{Binding ElementContextMenuVisibility}" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
CS :
public partial class View : UserControl
{
private ViewModel _viewModel = ViewModel.Instance;
private Visibility _elementContextMenuVisibility { get; set; }
public Visibility ElementContextMenuVisibility
{
get { return _elementContextMenuVisibility; }
set { _elementContextMenuVisibility = value; }
}
public View()
{
InitializeComponent();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_viewModel.ElementList != null && _viewModel.ElementList.Count > 0)
ElementContextMenuVisibility = Visibility.Visible;
else
ElementContextMenuVisibility = Visibility.Collapsed;
}
}
Thanks
You can achieve this with RelativeSource binding and no need of xaml.cs code.
XAML
<ListBox ItemsSource="{Binding ElementList}"
SelectedItem="{Binding SelectedElement}">
<ListBox.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Name="Add" Click="Add_Click" Header="Add element" />
<MenuItem Name="Delete" Click="Delete_Click"
HeaderStringFormat="Delete element {0}"
Header="{Binding SelectedElement.Name}"
Visibility="{Binding PlacementTarget.HasItems, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BooleanToVisibilityConverter}}" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>

Command binding in nested data template + wpf + mvvm

I need to pass values from view to view model still preserving mvvm. Let me explain the issue.
<UserControl x:Class="SelectorView" ...>
<ListView Name="gridListView" ItemsSource="{Binding... }">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Focusable" Value="false"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Border>
<StackPanel Orientation="Vertical">
<Label x:Name="vLabel" Content="{Binding VCValue}"/>
<ListView Name="checkBoxListView" ItemsSource="{Binding CList}">
<ListView.ItemTemplate>
<DataTemplate>
<CheckBox Command="{Binding DataContext.OnCheckedCommand,RelativeSource={RelativeSource Mode=Self}}" Margin="5" Click="CheckBox_Click" IsChecked="{Binding SelectedValue, Mode=TwoWay}" Content="{Binding Current, Mode=OneWay }"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</UserControl>
In above code, When user checks/unchecks the checkbox, i need to send value(content) of checkbox and also value of vLabel from view to view model. Both are within nested Data template. Earlier I used visualtreehelper to retrieve the same but I need to do this more in mvvm format. Hence, want to know if its doable using Command binding and how ? In check box, i binded to OnCheckedCommand but that doesn't seem to invoke OnCheckedExecuted() on check/uncheck.
So, how do i get the content of checkbox and vLabel on check/uncheck?
public class MainViewModel
{
public DelegateCommand<object> OnCheckedCommand { get; private set; }
public MainViewModel()
{
OnCheckedCommand = new DelegateCommand<object>(OnCheckedExecuted, CanCheck);
}
private void OnCheckedExecuted(object parameter)
{
}
private bool CanCheck(object parameter)
{
return true;
}
}
Any help is appreciated!

Cannot Access Command in WPF UserControl's DataGrid Row

I am not able to execute Command (TestCommand) in user control may be because DataGrid of usercontrol using FileDetailsList(List)
The following is the wpf form and i am using MVVM
<Window>
<TabControl>
<TabItem Header="Result">
<USERCONTROL:FileSearchResult></USERCONTROL:FileSearchResult>
</TabItem>
</TabControl>
</Window>
The below is the user control
<UserControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions
<DataGrid Grid.Row="0" ItemSource="{Binding FileDetailsList}">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button>View
<Button.ContextMenu>
<ContextMenu FontSize="11">
<MenuItem Command="{Binding TestCommand}" CommandParameter="{Binding FileId}" Header="Splitter Errors"/>
</ContextMenu>
</Button.ContextMenu>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding FileId}" Header="File ID"/>
<DataGridTextColumn Binding="{Binding FileName}" Header="File Name"/>
<DataGrid.Columns>
<DataGrid Grid.Row="0">
</Grid>
</UserControl>
The below is the view model
public class FileDetailsViewModel : INotifyPropertyChanged
{
private List<FileDetail> _fileDetailsList = new List<FileDetail>();
public RelayCommand<Int32> TestCommand { get; private set; }
public FileDetailsViewModel()
{
TestCommand = new RelayCommand<int>(OpenTestCommand);
}
private void OpenTestCommand(int fileId)
{
///Some code
}
public List<FileDetail> FileDetailsList { get { return _fileDetailsList; } set { _fileDetailsList = value; NotifyPropertyChanged("FileDetailsList"); } }
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Please help solve this issue
you try this it will work 100%,
<Button Tag="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}">
View
<!-- Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=DataContext. -->
<Button.ContextMenu>
<ContextMenu FontSize="11">
<MenuItem Command="{Binding Path=PlacementTarget.Tag.TestCommand,
RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
CommandParameter="{Binding FileId}"
Header="Splitter Errors" />
</ContextMenu>
</Button.ContextMenu>
</Button>
use menuitem command binding like this,
<MenuItem Command="{Binding DataContext.TestCommand, RelativeSource={RelativeSource AncestorType=DataGrid }}"
WPF rule number 1:
When binding does not work, check your output window!
you would see error saying, that TestCommand does not exist in FileDetails. This is because DataContext of each row in DataGrid and all descendant elements including your ContextMenu is not FileDetailsViewModel but FileDetail
easiest workaround is to move TestCommand to FileDetail class.
alternativelly, you can modify the binding to point to FileDetailsViewModel. Give name to root element in your UserControl or Window, e.g: LayoutRoot. Then use ElementName in the binding:
<MenuItem Command="{Binding LayoutRoot.DataContext.TestCommand, ElementName=LayoutRoot}"
CommandParameter="{Binding FileId}"
Initialized="ContextMenu_Initialized"/>
private void ContextMenu_Initialized(object sender, EventArgs e)
{
NameScope.SetNameScope((ContextMenu)sender, NameScope.GetNameScope(this));
}
another way how to fix the binding is workaround proposed by #jobyjames85, but it is little hacky. On the other side it is plain xaml solution with no codebehind involved

Binding property in ContextMenu from Parent's Parent's DataContext

I wanted to bind a property in a MenuItem within an ItemsControl container. I have a hierarchical model ; Item class has a list of SubItem. The ViewModel itself has a list of Item (so there's two ItemsControl, one being in the ItemTemplate of the other).
I found several other questions on SO asking about that (this one for instance) and I learned that the Visual Tree of a ContextMenu is separated from the rest.
I managed to do it and it works (but it feels kind of hacky) by "transferring" the model's data via the Tag property.
Here's the two model classes:
public class SubItem
{
public int Current { get; set; }
public Subitem(int current)
{
Current = current;
}
}
public class Item
{
public ObservableCollection<SubItem> SubItems { get; set; }
public string Parent { get; set; }
public Item(string Parent)
{
Parent = Parent;
SubItems = new ObservableCollection<SubItem>();
}
}
Here's the view model:
public class ViewModel
{
public ObservableCollection<Item> Items { get; set; }
public ViewModel()
{
Items = new ObservableCollection<Item>();
FillData();
}
private void FillData()
{
//...
}
}
And here's the ItemsControl at the root of the page (the page's DataContext is an instance of the ViewModel class):
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding SubItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Current}"
Tag="{Binding DataContext.Parent, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}, Mode=FindAncestor}}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}, Mode=FindAncestor}}"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
My question is: is this the right way to do it? I tried many other ways to avoid binding the property to the Tag but couldn't make it work.
The ugly part is specifically:
Tag="{Binding DataContext.Parent, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}, Mode=FindAncestor}}"
Followed by:
Header="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}, Mode=FindAncestor}}"
I think it's kind of ugly and I'm sure there's a better way to do that. The solution must work with .NET 4.0.
You need to add a tag to the menu's container and bind to it using placement target.
View this example:
<StackPanel x:Key="ConfigurationListItem" x:Shared="False" Tag="{Binding ElementName=UserControl}">
<StackPanel Orientation="Horizontal">
<Button>
<Button.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding ElementName=UserControl, Path=LaunchCommand}" CommandParameter="{Binding}" />
<MouseBinding Gesture="LeftClick" Command="{Binding ElementName=UserControl, Path=SelectCommand}" CommandParameter="{Binding}" />
</Button.InputBindings>
</StackPanel>
<StackPanel.ContextMenu>
<ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}" Tag="{Binding}">
<MenuItem Header="Sync Environment Dependencies"
Command="{Binding Parent.PlacementTarget.Tag.SyncEnvironmentCommand, RelativeSource={RelativeSource Self}}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, Path=PlacementTarget.DataContext}" />
</ContextMenu>
</StackPanel.ContextMenu>
</StackPanel>

Categories

Resources