I have an ItemsControl
that creates CheckBox
es based on DigitalFilterSubsystems
. I want to trigger an event handler on the viewmodel whenever a CheckBox
is clicked. At them moment my Click
property of the CheckBox
is complaining that Checked_Triggered
is expecting a property. Could someone explain what I am doing wrong and show a solution?
<ItemsControl Grid.Column="1" Grid.Row="0" HorizontalAlignment="Center" ItemsSource="{Binding DigitalFilterSubsystems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock HorizontalAlignment="Center" Text="{Binding }" Foreground="Black" FontSize="12"/>
<CheckBox HorizontalAlignment="Center" Click="{Binding Path=Checked_Triggered, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type vm:SimulatorControlViewModel}}, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In my viewmodel called SimulatorControlViewModel
:
public void Checked_Triggered(object sender, RoutedEventArgs e)
{
}
CodePudding user response:
You cannot bind to a method that way, you can only bind to properties. Depending on whether you are using some kind of MVVM framework there could be a solution offered by that framework. Another way would be to use a binding to a command property defined in your viewmodel. Have a look here: https://stackoverflow.com/a/3531935/1213316
CodePudding user response:
First of all, you cannot bind an event handler to an event in XAML. Even if it was possible, your binding would be wrong. Your view model is set as the DataContext
of a control, but RelativeSource
is used to refer to controls. What you would do is specify the type of the parent control as AncestorType
, that has the desired data context set and specify the property path like DataContext.YourPropertyToBind
, e.g.:
<CheckBox HorizontalAlignment="Center" IsChecked="{Binding DataContext.MyIsCheckPropertyOnTheViewModel, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
Now to the core of your issue, executing an action in the view model if the CheckBox
Click
event is raised. There are multiple options, depending on your requirements.
Expose a Property
If the items in your DigitalFilterSubsystems
are a custom type, e.g. Subsystem
, you can expose a property IsChecked
(or give it a more meaningful name for what checked means). In the setter of the property, call a method that acts on the current value of the property.
public class Subsystem : INotifyPropertyChanged
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set
{
if (_isChecked == value)
return;
_isChecked = value;
OnPropertyChanged(nameof(IsChecked));
OnCheckedChanged();
}
}
private void OnCheckedChanged()
{
if (IsChecked)
// ...do something.
else
// ...do something.
}
// ...other code.
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then simply bind the property in XAML to IsChecked
of CheckBox
.
<CheckBox HorizontalAlignment="Center" IsChecked="{Binding IsChecked}"/>
Use a Command
You can use a command to encapsulate the logic and expose it as a bindable object. For that you need a relay command or often called delegate command implementation. You can read about the details of these concepts in the following article.
A relay command simply takes an action that it encapsulates as an object and optionally a can execute predicate that determines if the action can be executed. This object can be exposed from a view model and bound in XAML. Relay commands implement the ICommand
interface and WPF knows how to handle it.
You can copy a relay command implementation from anywhere, there are plenty of implementations, most MVVM frameworks already provide their own. If you have chosen one, expose a command property in your SimulatorControlViewModel
and initialize it in the constructor with the method to be called (this could vary depending on the concrete command implementation that you use).
public class SimulatorControlViewModel : INotifyPropertyChanged
{
public SimulatorControlViewModel()
{
Clicked = new RelayCommand(ExecuteClicked);
}
public ICommand Clicked { get; }
private void ExecuteClicked(object isChecked)
{
if ((bool)isChecked)
// ...do something.
else
// ...do something.
}
// ...other code.
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then bind the Clicked
command to the Command
property of the CheckBox
and bind the CommandParameter
to its own IsChecked
property (this is the parameter that is passed to the ExecuteChecked
method.
<CheckBox HorizontalAlignment="Center" Command="{Binding Clicked, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" CommandParameter="{Binding IsChecked, RelativeSource={RelativeSource Self}}"/>
Use a Behavior to Call A Method
You can install the Microsoft.Xaml.Behaviors.Wpf NuGet package, which contains CallMethodAction
that allows a method to be called on an object when an event is raised.
<CheckBox HorizontalAlignment="Center">
<b:Interaction.Triggers>
<b:EventTrigger EventName="Click">
<b:CallMethodAction MethodName="Checked_Triggered" TargetObject="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"/>
</b:EventTrigger>
</b:Interaction.Triggers>
</CheckBox>
Caution: The method signature must match public void Checked_Triggered()
exactly (public
, return void
and no arguments), otherwise you will get an exception stating that the method was not found.