Pull to refresh

Детектор блокировок UI в WPF c нотификацией

Reading time3 min
Views14K


Приветствую!

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

Всем спасибо!
Tags:
Hubs:
+18
Comments29

Articles

Change theme settings