Home > Software design >  Binding Checkbox Click event inside ItemsControl to viewmodel
Binding Checkbox Click event inside ItemsControl to viewmodel

Time:10-13

I have an ItemsControl that creates CheckBoxes 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.

  • Related