C# UWP XAML- Updating controls “synchronously”

2019-04-17 17:24发布

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.

2条回答
Summer. ? 凉城
2楼-- · 2019-04-17 17:55

In a nutshell, consider these tips as you build your app:

  • Never call .Wait() or Result or otherwise try to block on an asynchronous operation on a UI dispatcher thread
  • If you do want to create worker threads to do blocking operations, try await Task.Run(...) for its simplicity (you can create raw threads but it's more work). Same for busy-waits like while(notDone) ; // empty
  • From a background thread (such as one created by Task.Run), if you want to update the UI then you would use Dispatcher.RunAsync(...) but only to set the properties of your UI
  • To disable your UI while a background thread does work, set IsEnabled=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.

查看更多
Rolldiameter
3楼-- · 2019-04-17 18:03

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.

查看更多
登录 后发表回答