Детектор блокировок UI в WPF c нотификацией
Приветствую!
Думаю что каждому из программистов попадалось приложение которое по тем или иным причинам блокировало UI. Причин у таких блокировок может быть множество, такие как: синхронные запросы к сервисам, выполнение долгих операций в UI треде и прочее.В самом лучшем случае участки кода приводящие к блокировкам UI должны быть переписаны / исправлены, но это не всегда возможно по разным причинам и соответственно хочется получить некую серебряную пулю, которая сможет решить проблему с минимальной стоимостью.О одной такой пуле и пойдет речь.
Подробности под катом.Определяем что UI заблокирован
Собственно определение того что заблокирован UI сводится к простому решению запустить два счетчика. Первый счетчик работает в главном треде приложения и ставит временные метки при каждом срабатывании. Второй счетчик работает в фоновом треде и вычисляет разницу между текущим временем и временем установленным первым счетчиком. Если разница между временами превышает определенный лимит, выбрасывается событие о том что UI заблокирован и наоборот, если UI уже не заблокирован выбрасываем событие о том что приложение ожило.Делается это так:
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
Для того чтобы показать пользователю сообщение о том что приложение работает, подписываемся на события от класса BlockDetector и показываем новое окно с сообщением о заблокированном UI.
WPF разрешает создавать несколько UI тредов. Делается это так:
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 (); } Делегат на создание окна нужен для того чтобы иметь возможность более гибкого подхода к окну нотификации.Более подробно прочитать о создании окна в отдельном треде можно почитать в этой статье Launching a WPF Window in a Separate Thread
РезультатНеобходимо оговорится что предложенное решение не является той самой серебряной пулей, которая подойдет абсолютно всем. Уверен, что в целом ряде случаев применить такое решение окажется невозможным по тем или иным причинам.Посмотреть как это все работает можно на подготовленном мной демо-проекте: yadi.sk/d/WeIG1JvEhC2Hw
Всем спасибо!