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 theIsDataAcquisitionActivated
! [...] I remember why it was commented out [...]: TheStopMeasurement
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.
- The
ToggleButton
is clicked. - The measurement is started,
IsDataAcquisitionActivated
is set totrue
from theToggleButton
. - A property changed notification is raised.
- The
ToggleButton
changes its state to Checked. - The
Checked
event is raised and invokes theStartMeasurementCommand
starting the timer. - The timer runs out and invokes
OnStopMeasurementExecute
. (First time). - The method sets
IsDataAcquisitionActivated
tofalse
. - A property changed notification is raised.
- The
ToggleButton
changes its state to Unchecked. - The
Unchecked
event is raised and invokes theStopMeasurementCommand
(Second time). - ...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.