Home > Software design >  How to reset Interaction.Triggers in toggle button correctly with code behind?
How to reset Interaction.Triggers in toggle button correctly with code behind?

Time:08-31

In my current C#/WPF project I use a toggle button with Interaction.Triggers to start and stop a measurement and it works as intended. You press the start button, it starts to measure, you press the stop button and it stops and resets the properties so you can do it again.

The process in the GUI looks like this:

XAML code:

<ToggleButton x:Name="ButtonMeasurementConnect" 
  Grid.Row="5" Grid.Column="3"
  VerticalAlignment="Center"
  Content="{Binding ButtonDataAcquisitionName}"
  IsChecked="{Binding IsDataAcquisitionActivated, Mode=TwoWay}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Checked">
            <i:InvokeCommandAction Command="{Binding StartMeasurementCommand}"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="Unchecked">
            <i:InvokeCommandAction Command="{Binding StopMeasurementCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ToggleButton>

Now I added the option to set a timer so the program automatically stops the measurement after a desired time like in this example.

You can see that when I click the start button, the timer stops at 1 second, the start button name property resets correctly to "Start" again instead of "Stop".

Here's finally the problem: If I want to repeat it I have to press the "Start" button twice. First time pressing again will just result in invoking the StopMeasurementCommand.

How can I tell the toggle button that it should reset to using the StartMeasurementCommand binding the next time it gets used inside the code behind, not by pressing the button manually?

EDIT: Here's the code inside the view model, first the obvious handling of the commands:

StartMeasurementCommand = new DelegateCommand(OnStartMeasurementExecute);
StopMeasurementCommand = new DelegateCommand(OnStopMeasurementExecute);

Here the OnStopMeasurementExecute:

try
{
    if (stopWatch.IsRunning)
    {
        stopWatch.Stop();
    }
    _receivingDataLock = true;

    // Stop writing/consuming and displaying
    _sourceDisplay.Cancel();
    if (IsRecordingRequested)
    {
        _sourceWriter.Cancel();
    } else
    {
        _sourceConsumer.Cancel();
    }

    // Sending stop command to LCA
    _dataAcquisition.StopDataAcquisition();

    // Flags
    ButtonRecordingVisibility = Visibility.Hidden;
    IsDataAcquisitionActivated = false;
    IsDataAcquisitionDeactivated = true;
    ButtonDataAcquisitionName = "Start";

    if (IsRecordingRequested)
    {
        StatusMessage = "Recording stopped after "   CurrentTime;
    }
    else
    {
        StatusMessage = "Live data stopped after "   CurrentTime;
    }

    if (IsRecordingRequested) _recordedDataFile.Close();

}
catch (Exception e)
{
    Console.WriteLine("Exception in OnStopMeasurementExecute: "   e.Message);
    
}

If a timer is set it gets invoked by the timer function as well:

void Stopwatch_Tick(object sender, EventArgs e)
{
    if (stopWatch.IsRunning)
    {
        TimeSpan ts = stopWatch.Elapsed;
        CurrentTime = String.Format("{0:00}:{1:00}:{2:00}", ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
        if ((IsTimerActivated) && (MeasureTime>0)) {
            if (ts.Seconds >= MeasureTime) OnStopMeasurementExecute();
        }
    }
}

CodePudding user response:

The cause of your issue is that the IsDataAcquisitionActivated is reset in the view model, but you do not raise a property changed notification. Therefore the bindings will not be notified of the change and hold their old value, which is false. This means, although the text on the button changes, it is still in Unchecked state, resulting in StopMeasurementCommand being executed.


I noticed the OnPropertyChanged(); was commented out for the IsDataAcquisitionActivated! [...] I remember why it was commented out [...]: The StopMeasurement function seems to get fired twice now which i can see because the "Live data stopped after ..." status message gets triggered twice now.

Correct. Let us review the sequence of events in this scenario to find the issue.

  1. The ToggleButton is clicked.
  2. The measurement is started, IsDataAcquisitionActivated is set to true from the ToggleButton.
  3. A property changed notification is raised.
  4. The ToggleButton changes its state to Checked.
  5. The Checked event is raised and invokes the StartMeasurementCommand starting the timer.
  6. The timer runs out and invokes OnStopMeasurementExecute. (First time).
  7. The method sets IsDataAcquisitionActivated to false.
  8. A property changed notification is raised.
  9. The ToggleButton changes its state to Unchecked.
  10. The Unchecked event is raised and invokes the StopMeasurementCommand (Second time).
  11. ...and so on.

The fundamental issue is to rely on events here while binding the IsChecked state two-way. It is way easier to do one or the other, if there is no requirement against it.

In the event sequence you see that the timer invokes the OnStopMeasurementExecute method twice through executing it and indirectly triggering the OnStopMeasurementExecute command from the ToggleButton. If you do not call the method, but only set the IsDataAcquisitionActivated property instead, it will only be called once.

if (ts.Seconds >= MeasureTime) IsDataAcquisitionActivated = false;

This does not require much adaption in your code, although I would prefer an approach that does not wire events to commands, since it is harder to comprehend and track.


Here are two alternative approaches with explicit event to command bindings.

1. Unify the Commands and Handle the State There

The ToggleButton only cares about the IsDataAcquisitionActivated property to display the correct state, Checked or Unchecked. It does not have to set the state, so let the command handle that. Let us use a one-way binding, since the view model is the source of truth here. Then let us combine the two separate commands to one, ToggleMeasurementCommand.

<ToggleButton x:Name="ButtonMeasurementConnect" 
              Grid.Row="5" Grid.Column="3"
              VerticalAlignment="Center"
              Content="{Binding ButtonDataAcquisitionName}"
              IsChecked="{Binding IsDataAcquisitionActivated, Mode=OneWay}"
              Command="{Binding ToggleMeasurementCommand}"/>

The ToggleMeasurementCommand now only delegates to a the start or stop method depending on the IsDataAcquisitionActivated property.

ToggleMeasurementCommand = new DelegateCommand(OnToggleMeasurementExecute);
private void OnToggleMeasurementExecute()
{
   if (IsDataAcquisitionActivated)
      OnStartMeasurementExecute();
   else
      OnStopMeasurementExecute();
}

Adapt the start and stop methods to set the correct state for IsDataAcquisitionActivated.

private void OnStartMeasurementExecute()
{
   IsDataAcquisitionActivated= false;
   //... your other code.
}

private void OnStopMeasurementExecute()
{
   IsDataAcquisitionActivated= true;
   //... your other code.
}

The property is set once from the view model and the ToggleButton only updates based on the property changed notifications it gets from the view model.

Another thought on ToggleButton: You could reconsider if you really need a ToggleButton. The text states an action Start or Stop, not a state (although there is one implicitly). Consequently with the single command, you could just use a simple Button, no need bind any state.

2. Act On Property Changes

You could react to property changes. Remove the commands and leave the two-way binding.

<ToggleButton x:Name="ButtonMeasurementConnect" 
  Grid.Row="5" Grid.Column="3"
  VerticalAlignment="Center"
  Content="{Binding ButtonDataAcquisitionName}"
  IsChecked="{Binding IsDataAcquisitionActivated, Mode=TwoWay}">
</ToggleButton>

Now the only indication when to start or stop the measurement is a change of the property IsDataAcquisitionActivated or in other words, when its setter is called with a changed value.

public bool IsDataAcquisitionActivated
{
   get => _isDataAcquisitionActivated;
   set
   {
      if (_isDataAcquisitionActivated == value)
         return;

      _isDataAcquisitionActivated= value;
      OnPropertyChanged();

      if (_isDataAcquisitionActivated)
         OnStartMeasurementExecute();
      else
         OnStopMeasurementExecute();
   }
}

Then of course, your timer would not call OnStopMeasurementExecute anymore, but only set the property, since the method will be called automatically then.

if (ts.Seconds >= MeasureTime) IsDataAcquisitionActivated = false;

CodePudding user response:

The mistake was that the function to implement INotifyPropertyChanged was not invoked in the property bound to the IsChecked attribute of the toggle button.

Now that it is set the timer in the code behind resets the button correctly as pressing the Stop button does.

One downside came up with this. For some reason the method invoked by the StopMeasurementCommand gets fired twice in a row but that is a different issue.

  • Related