Throughout this question, I've included some links which show that I've done some work searching for a solution.
I'm developing a UWP app with touchscreen and GPIO.
UI has a stop button, a reset button, and a textblock. GPIO is used for a physical start button, a motor, and 2 limit switches. Motor can rotate until it runs into a limit switch.
Code to control the hardware (e.g., Motor.Forward()) has been written and tested, but is excluded from this question for brevity. Code for the stop button is excluded for the same reason.
If the steps in these methods would perform synchronously... desired behavior might be described by the following code:
//Event handler for when physical start button is pushed
private async void StartButtonPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
{
Start();
}
private void Start()
{
//Update UI
stopButton.IsEnabled = true;
resetButton.IsEnabled = false;
textBlock.Text = "Motor turning";
Motor.Forward();
while(Motor.HasNotHitEndLimit())
Motor.Off();
//Update UI
stopButton.IsEnabled = false;
resetButton.IsEnabled = true;
textBlock.Text = "Task complete";
}
//Event handler for reset button
private void btnReset_Click()
{
//Update UI
stopButton.IsEnabled = true;
resetButton.IsEnabled = false;
textBlock.Text = "Motor turning";
Motor.Back();
while(Motor.HasNotHitStartLimit())
Motor.Off();
//Update UI
stopButton.IsEnabled = false;
resetButton.IsEnabled = true;
textBlock.Text = "Reset complete";
}
If I recall correctly, UI updates within "private void btnReset_Click()" work, but they are not synchronous... I.e., all of the UI updates were completing right after "btnReset_Click()" finished.
From reading answers to similar questions... it seems that UI updates within "Start()" fail because I'm not on the UI thread ("The application called an interface that was marshalled for a different thread."). It seems that Task Asynchronous Pattern is a common answer to these types of questions. However, my attempts to do this have yielded strange results...
The code below is the closest I've come to the desired result. I added async tasks that use CoreDispatcher to handle UI updates.
//Task for updating the textblock in the UI
private async Task UpdateText(string updateText)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
new DispatchedHandler(() => { textBlock.Text = updateText; }));
}
//Task for enable/disable a given button
private async Task UpdateButton(Button btn, bool shouldThisBeEnabled)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
new DispatchedHandler(() => { btn.IsEnabled = shouldThisBeEnabled; }));
}
//Event handler for when physical start button is pushed
private async void StartButtonPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
{
Start();
}
private void Start()
{
//Update UI
UpdateButton(stopButton,true).Wait();
UpdateButton(resetButton,false).Wait();
UpdateText("Motor turning").Wait();
Motor.Forward();
while(Motor.HasNotHitEndLimit())
Task.Delay(1).Wait();
Motor.Off();
//Update UI
UpdateButton(stopButton,false).Wait();
UpdateButton(resetButton,true).Wait();
UpdateText("Task complete").Wait();
}
//Event handler for reset button
private async void btnReset_Click()
{
//Update UI
await UpdateButton(stopButton,true);
await UpdateButton(resetButton,false);
await UpdateText("Motor turning");
await Task.Delay(1);
Motor.Back();
while(Motor.HasNotHitStartLimit())
await Task.Delay(1);
Motor.Off();
//Update UI
await UpdateButton(stopButton,false);
await UpdateButton(resetButton,true);
await UpdateText("Reset complete");
}
Problems/idiosyncrasies with the code above (besides any beginner mistakes I might be making due to just starting out with C#... and the fact that it seems overly complicated and confusing):
-In "Start()" I use .Wait() on the tasks (because it seems to work, I don't really understand why...), and in btnReset_Click() it worked best to await them...
-btnReset_Click() is not synchronous. UI updates appear to be "one step behind"... I.e., in debug mode, the stop button enables when I step over "await UpdateButton(resetButton,false)", reset button disables when I step over "await UpdateText("Motor turning")", and so on.
-Regarding btnReset_Click()... The while loop lasts MUCH longer than 1 millisecond in real time, yet if I remove all "await Task.Delay(1)" then the UI updates are "one step behind". With "await Task.Delay(1)" included, the UI updates get "caught up" to where they should be. Why does "await Task.Delay(1)" affect UI updates this way?
If any knowledgeable folks are willing to address some/all of this question and maybe let me prod them for details about their answer(s), I'd be very grateful!
Bonus question.... I also have a "Toggle Test Mode" button on the touchscreen which enables one list of UI buttons and disables another (based on a static bool "testmode"). I don't need to use TAP to update the UI here, but recall that I want to do this synchronously (even though it seems pointless in this example).
private async void btnTestMode_Click(object sender, RoutedEventArgs e)
{
testMode = !testMode;
if (testMode == true)
{
await UpdateButtons(TestModeButtons,true);
await UpdateButtons(NormalModeButtons,false);
return;
}
await UpdateButtons(TestModeButtons,true);
await UpdateButtons(NormalModeButtons,false);
}
private async Task UpdateButtons(List<Button> btns, enable)
{
foreach (var btn in btns)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
new DispatchedHandler(() => { btn.IsEnabled = enable; }));
}
}
As it's written above, this behaves like btnReset_Click()... where the UI updates are "one step behind". However, if I add ".ConfigureAwait(false)" to each await in the event handler, then it becomes synchronous. I've done some reading on this topic, but don't fully understand it yet, and I would love for someone with a better understanding to help me understand it as it relates to my project.
In a nutshell, consider these tips as you build your app:
.Wait()
orResult
or otherwise try to block on an asynchronous operation on a UI dispatcher threadawait Task.Run(...)
for its simplicity (you can create raw threads but it's more work). Same for busy-waits likewhile(notDone) ; // empty
Task.Run
), if you want to update the UI then you would useDispatcher.RunAsync(...)
but only to set the properties of your UIIsEnabled=false
or add a top-most emi-transparent interaction shield etc.Also, try starting with something simple (eg, no hardware access; just use
Task.Delay(...).Wait()
to simulate blocking on the hardware). Once you have the UI basically working you can plug in the hardware calls.You should not be doing any of that Dispatcher.Run and similar...
First stop and think and understand what your problem is and why the UI does not update.
Create a new thread where you control your motors (separate from the UI thread).
On the button clicks, call method on your motors thread.
When there are events on the motors thread where you need to update the UI, call (synchronously?) methods on the UI thread.