-2

We're building a wpf application. We have pretty loading indicators. I noticed they "lag" sometimes a while ago, but it wasn't a huge deal. We make heavy use of the async await keywords to keep the application responsive during heavy work.

We had some performance problem which caused me to look deeper into how async await, the UI-Thread and the application freezing connect.

The way I understand it, mainly based on this post If async-await doesn't create any additional threads, then how does it make applications responsive?, is that my loading indicator can only spin, while no other code is being executed on the UI Thread. Trivial code will execute fast enough that you don't see any "lag".

I thought I had found the problem. We thought async await was a magic pattern that prevents the UI from freezing.

To fix it, I need to move the blocking code into actual background threads. But I cannot do that, because most of that code has to do with accessing either UI elements like Windows or UserControls or accessing the Binding properties, all of which cannot be manipulated from outside the UI-Thread.

I can only create Windows on the UI-Thread. So how would I do something as simple as showing a responsive splash screen?

var splashScreen = new SplashScreen();
splashScreen.Show();

var startUpWindow = new StartUpWindow(); // lets say this always takes 2 seconds
startUpWindow.Show(); // lets say this always takes 1 second

I cannot get around doing this on the UI Thread as far as i can tell. Any amount of trickery via async await, or Task.Run into Dispatcher.Invoke will always end up executing this code on the UI-Thread which will in turn always freeze my application.

This seems like a pretty big oversight to me. Too big to exist. How can a UI framework freeze every time I open a new Window or add a new control or am forced to use the UI-Thread for any other reason? Am I missing something?

Edit: Yes background threads stop the freezing. Also my application is probably not perfect. The point is, you cannot create/show windows on background threads. You cannot access ObservableCollections on background threads.

Try fixing my 4 line code example. Its not a complex or contrived example. The way I see it currently its unsolvable.

DFENS
  • 289
  • 1
  • 12
  • Have you tried creating a [BrackgroundWorker](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/backgroundworker-component-overview?view=netframeworkdesktop-4.8)? (side note: making an application always responsive is hard work, and has been since the beginning of graphical user interfaces for the most part, from my experience) – crashmstr Nov 20 '20 at 18:43
  • 1
    You may want to execute the initialization routine inside `StartUpWindow` asynchronously or on a background thread. You can add a `Initialized` event to `StartUpWindow`. Inside the event handler you close the splash screen and show the initialized window. – BionicCode Nov 20 '20 at 18:45
  • In general it depends on the consumed resources of the UI thread. When you flood the UI thread or more specifically the dispatcher queue, then there are no resources left to handle elementary tasks like input or render events => the GUI appears to lag or freeze. That's why you usually try to move heavy CPU work to a worker thread or to execute I/O operations asynchronously. – BionicCode Nov 20 '20 at 18:45
  • If you have to initialize the visual tree manually (child controls) before they are actually rendered, you should revisit your UI design and consider to refactor the code. You should leave initialization of controls to the framework or use a better timing e.g. `FrameworkElement.Loaded` event. Maybe you should rely more on data templates to display controls dynamically and only initialize the data models. Most layout related operations are executed asynchronously by the framework. It doesn't sound well implemented if you have to touch bindings and UIElements before they are rendered. – BionicCode Nov 20 '20 at 18:52
  • 1
    See my first comment to fix your four lines of code. You can't omit the fact that every `DispatcherObject` has thread/dispatcher affinity and must be instantiated on a dispatcher thread. For freezables you can create those objects on a background thread and pass them to the UI thread once you called `Freeze()`. But you need to move CPU heavy work to a background thread. You create `StartUpWindow` on the UI thread, but internally `StartUpWindow` would move its initialization routine to a worker thread. – BionicCode Nov 20 '20 at 19:06
  • Ohhh @BionicCode now I see. My window creation takes about 2 seconds, i assumed that's mainly just the window and binding system booting up. But looking at exactly what you said, its actually resolving the ViewModel from the dependency container, that takes over 90% of those 2 seconds. – DFENS Nov 20 '20 at 19:26
  • You can access observablecollection on a background thread, but you don't need to. Push your data gathering onto background threads. Return the data from there. Set properties on your viewmodel. With collections of data, use return a list of row viewmodels from your thread. Use the observablecollection ctor that takes a list to new up an observablecollection and set your bound property to that. It then raises on propertychanged and the collection is read into itemssource. But filter large sets of data to less than 200 records. – Andy Nov 21 '20 at 10:59

1 Answers1

1

The point is, you cannot create/show windows on background threads.

Well, yes and no. It's true that you cannot access UI objects from background threads. However, you can create another UI thread. Each UI object would then belong to one of the two specific UI threads. The official docs on the WPF threading model describe how to do this.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810