📜 ⬆️ ⬇️

Detector of UI locks in WPF with notification



Greetings

I think that each of the programmers came across an application that, for one reason or another, blocked the UI. The reasons for such locks can be many, such as: synchronous requests to services, performing long operations in the UI thread, and so on.
In the best case, parts of the code leading to UI locks should be rewritten / corrected, but this is not always possible for various reasons and, accordingly, I want to get some kind of silver bullet that can solve the problem with minimal cost.
About one such bullet and will be discussed.
')
Details under the cut.

Determine that UI is blocked.

Actually determining that a UI is blocked is reduced to a simple solution to start two counters. The first counter works in the main thread of the application and puts time stamps on each trigger. The second counter runs in a background thread and calculates the difference between the current time and the time set by the first counter. If the difference between the times exceeds a certain limit, an event is thrown indicating that the UI is blocked and vice versa, if the UI is not already blocked, throw out the event that the application has come to life.
This is done like this:
internal class BlockDetector { bool _isBusy; private const int FreezeTimeLimit = 400; private readonly DispatcherTimer _foregroundTimer; private readonly Timer _backgroundTimer; private DateTime _lastForegroundTimerTickTime; public event Action UIBlocked; public event Action UIReleased; public BlockDetector() { _foregroundTimer = new DispatcherTimer{ Interval = TimeSpan.FromMilliseconds(FreezeTimeLimit / 2) }; _foregroundTimer.Tick += ForegroundTimerTick; _backgroundTimer = new Timer(BackgroundTimerTick, null, FreezeTimeLimit, Timeout.Infinite); } private void BackgroundTimerTick(object someObject) { var totalMilliseconds = (DateTime.Now - _lastForegroundTimerTickTime).TotalMilliseconds; if (totalMilliseconds > FreezeTimeLimit && _isBusy == false) { _isBusy = true; Dispatcher.CurrentDispatcher.Invoke(() => UIBlocked()); ; } else { if (totalMilliseconds < FreezeTimeLimit && _isBusy) { _isBusy = false; Dispatcher.CurrentDispatcher.Invoke(() => UIReleased()); ; } } _backgroundTimer.Change(FreezeTimeLimit, Timeout.Infinite); } private void ForegroundTimerTick(object sender, EventArgs e) { _lastForegroundTimerTickTime = DateTime.Now; } public void Start() { _foregroundTimer.Start(); } public void Stop() { _foregroundTimer.Stop(); _backgroundTimer.Dispose(); } } 


UI blocking message

In order to show the user a message stating that the application is working, we subscribe to events from the BlockDetector class and display a new window with a message about the blocked UI.

WPF allows you to create multiple UI threads. This is done like this:
 private void ShowNotify() { var thread = new Thread((ThreadStart)delegate { //      _threadDispacher = Dispatcher.CurrentDispatcher; SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(_threadDispacher)); //    _notifyWindow = _createWindowDelegate.Invoke(); //          _notifyWindow.Closed += (sender,e) => _threadDispacher.BeginInvokeShutdown(DispatcherPriority.Background); _notifyWindow.Show(); //    Windows   Dispatcher.Run(); }); thread.SetApartmentState(ApartmentState.STA); thread.IsBackground = true; thread.Start(); } 


The delegate to create a window is needed in order to have a more flexible approach to the notification window.
You can read more about creating a window in a separate thread in this article. Launching a WPF Window in a Separate Thread

Result
It is necessary to make a reservation that the proposed solution is not the same silver bullet that will suit absolutely everyone. I am sure that in a number of cases such a solution will not be possible for one reason or another.
You can see how it all works on the demo project I prepared: yadi.sk/d/WeIG1JvEhC2Hw

Thanks to all!

Source: https://habr.com/ru/post/260029/


All Articles