Изображение обновляется гораздо реже, чем состояние ViewModel. Можно ли это пофиксить?

Лет 10 не писал на WPF, да и не сильно хорошо тогда его изучил. И тут понадобилось опять на нём написать задачку. Нагородил возможно велосипедов, хотелось бы разобраться, как сделать по уму.

В общем, в XAML есть сторонний контрол для быстрого отображения графиков ScottPlot в варианте для WPF. Он довольно ограниченный и не умеет жить во ViewModel, а только в code behind. Я не разобрался, как можно обновить что-то в code behind из View Model, поэтому во View Model делаются какие-то расчёты, а в code behind висит DispatcherTimer, по тикам таймера проверяется, выставил ли я флажок во ViewModel, и если выставил, то происходит отрисовка в контроле. В целом это выглядит в упрощённом виде так:

     Slider перемещается 
               |
               V
обновляются данные ViewModel
               |
               V
отрисовка контрола в code behind

При этом реакция на движение слайдера выглядит какой-то дёрганной. Даже если таймер запускать вообще без задержки. Стал разбираться, добавил статус бар, в котором стал писать всякую статистику. Выяснилось, что расчёты выполняются меньше чем за 1мс, а рендер контрола в code behind 5мс. При этом, само обращение к расчётам происходит раз в 10 чаще, чем рендер. Если уменьшать задержку таймера, то соотношение рендеров к расчётам становится чуть лучше, но не намного. Либо сам таймер срабатывает тоже с какой-то задержкой, либо я ещё чего-то не понимаю.

Итак, вопросы:

  1. Может есть какой-то другой способ инициировать обновление контрола ScottPlot.WPF в code behind, без участия таймера? Как правильно послать сигнал из ViewModel в code behind?
  2. Либо может как-то DispatcherTimer починить/заменить, чтобы он более часто срабатывал? Да хорошо ли это будет для приложения в целом - всё время проверять в code behind какой-то флаг без остановки? В то время, когда есть всякие биндинги казалось бы специально для этого.
  3. Либо я вообще всю концепцию неправильно понимаю и нужно всё как-то совсем по-другому делать. Может есть какой-то пример правильного кода для WPF, который что-то графическое отрисовывает, реагируя на Slider или другой подобный контрол в реальном времени без задержек?

Конечно, если бы этот контрол дружил с WPF, то можно было бы его забиндить на поле во ViewModel и он бы сам сразу отображался. Со ScottPlot.WPF так, к сожалению, нельзя сделать, об этом прямо в документации написано, что это сознательное решение. Рисуем быстро, но с широким функционалом WPF не дружим.

Ещё есть вариант в XAML повесить Image, а во ViewModel через функции ScottPlot делать рендер в bmp и потом подставлять готовую картинку в этот Image, я так ещё один контрол уже сделал. Но тогда теряются фичи ScottPlot.WPF по зуму и скроллированию графиков. Может они мне и не нужны будут, эти фичи, но пока не хотел уходить совсем в этот вариант.

Да, и ещё конечно можно повесить в code behind обработчики событий слайдера и делать все пересчёты там, а следом рендер. Но как-то не хотелось бы на такой низкий уровень совсем спускаться от биндингов без крайней необходимости.


Ответы (1 шт):

Автор решения: aepot

Зачем вообще рендер по таймеру?

Берёте INPC вьюмодель, реальзуете вызов события ProperyChanged, подписываетесь на него в юзерконтроле, и на каждое изменение любого свойства тригерите рендер, сами из кода.

public class MyViewModel : INotifyPropertyChanged
{
    private bool _myFlag;
    
    public bool MyFlag
    {
        get => _myFlag;
        set
        {
            if (_myFlag != value)
            {
                _myFlag = value;
                OnPropertyChanged();
            }
        }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Теперь подписаться

MyViewModel vm = new();
vm.PropertyChanged += OnVmPropertyChanged;
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(MyViewModel.MyFlag))
    {
        Render();
    }
}

Если нужно отследить добавление/удаление элементов ObservableCollection, так же подпишитесь на CollectionChanged у неё.

Только подписываясь на события не забудьте отписываться, когда контрол покидает визуальное дерево или диспозится.

→ Ссылка