Приветствую!
Думаю что каждому из программистов попадалось приложение которое по тем или иным причинам блокировало 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
Всем спасибо!
ссылка на оригинал статьи http://habrahabr.ru/post/260029/
Добавить комментарий