
Приветствую!
Думаю что каждому из программистов попадалось приложение которое по тем или иным причинам блокировало 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
Всем спасибо!