https://blog.benoitblanchon.fr/wpf-high-speed-mvvm/

[WPF/MVVM] How to deal with fast changing properties

17 December 2013 csharp, wpf, mvvm

In this article, I will describe a problem which happens in a WPF/MVVM application when the Model is updated at a very high frequency.

It happened to me while implementing a Model handling a lot of values coming from instruments and I wanted to display the current values, even if they were changing very quickly.

The test case

All my test are based on this simple Model class:

public class Model
{
    const long MAX_DURATION = 20000;

    public double Progress { get; private set; }
    public event EventHandler<double> ProgressChanged;

    public double Frequency { get; private set; }
    public event EventHandler<double> FrequencyChanged;        

    public Model()
    {
        Task.Run((Action)LongRunningBackgroundTask);
    }
    
    void LongRunningBackgroundTask()
    {
        long loopCount = 0;
        long elapsed = 0;

        var chrono = new Stopwatch();
        chrono.Start();

        while (elapsed < MAX_DURATION)
        {
            elapsed = chrono.ElapsedMilliseconds;

            SetProgress(100.0 * elapsed / MAX_DURATION);
            SetFrequency(1.0 * loopCount / elapsed);

            loopCount++;
        }
    }

    void SetProgress(double value)
    {
        Progress = value;

        if (ProgressChanged != null)
            ProgressChanged(this, value);
    }

    void SetFrequency(double value)
    {
        Frequency = value;

        if (FrequencyChanged != null)
            FrequencyChanged(this, value);
    }
}

As you can see, it’s very straightforward. The Model class contains two public properties Progress and Frequency and their associated events. During 20 seconds, it runs a loop in a background task and updates the properties as fast as it can:

  • Progress will go from 0.0 to 100.0.
  • Frequency will contains the average loop frequency so far.

The problem with the classic MVVM approach

In the classic MVVM approach, the ViewModel is attached to the Model’s events, so as to be updated on every change.

Usually, the ViewModel’s event handler calls Dispatcher.BeginInvoke() to ensure the event PropertyChanged is raised in the UI thread.

public ViewModel()
{
    dispatcher = Dispatcher.CurrentDispatcher;
    model = new Model();

    model.ProgressChanged += OnModelProgressChanged;
    // ... then the same for the Frequency
}
    
void OnModelProgressChanged(double newValue)
{
    dispatcher.BeginInvoke((Action)delegate() { Progress = newValue; });
}

public double Progress
{
    get { return progress; }
    set
    {
        if( progress == value ) return;
        progress = value;
        RaisePropertyChanged("Progress");
    }
}

// ... then the same pattern for the Frequency property

However, this approach wont be able to work with the Model class defined earlier. The GUI is completely frozen and sometimes even throws an OutOfMemoryException.

Classic MVVM: GUI is frozen during 20 seconds

Classic MVVM: the Task execution speed is poor

Here is why: Each time a Model’s property changes, the ViewModel calls BeginInvoke() and therefore appends a message in the dispatcher’s event queue. But the messages are dequeued way slower than they are added, so the queue will grow over and over until the memory is full.

Also, you can see that the execution speed of the Model’s task is really affected : only 130 kHz on average.

Solution 1 : Ignore events that are too close

The first solution that usualy comes in mind is:

Hmmm… I get too many events…

I’ll just slow them down !

OK, let’s try…

public ViewModel()
{
    var dispatcher = Dispatcher.CurrentDispatcher;
    var model = new Model();

    Observable.FromEventPattern<double>(model, "ProgressChanged")
            .Sample(TimeSpan.FromMilliseconds(5))
            .ObserveOn(dispatcher)
            .Subscribe(x => Progress = x.EventArgs);
}

Here, I used Reactive Framework because it offers the Sample() method which limits the rate of the events.

Solution1: GUI is responsive but Task speed is slow

In this case the GUI is perfectly responsive and the Task execution speed is better but still low.

I think it’s a viable if you already use Reactive Framework, but I wouldn’t use it in my project: it’s too complicated and the performance is not good enough.

Solution 2 : Poll with a DispatcherTimer

Let’s look a this problem from a different angle. Why don’t we loose the “push” approach and use “pull” approach instead ?

In other words, instead of attaching to the event of the Model, the ViewModel could periodically read the values.

The most common way to implement polling in MVVM is to instanciate a DispatcherTimer in the ViewModel.

public ViewModel()
{
    model = new Model();

    var timer = new DispatcherTimer();
    timer.Interval = TimeSpan.FromMilliseconds(5);
    timer.Tick += OnTimerTick;
    timer.Start();
}

void OnTimerTick(object sender, EventArgs e)
{
    Progress = model.Progress;
    Frequency = model.Frequency;
}

// ...the remaining is identical to the original ViewModel

Here you go ! No only the GUI is perfectly responsive, the execution speed of the Task is way better: 10 MHz

Solution2: good responsiveness and good speed

Solution 3 : Poll on CompositionTarget.Rendering

To make it even simpler, we can move the timer from the ViewModel to the View. From that place, we can use the CompositionTarget.Rendering event and completely get rid of the DispatcherTimer. (As a reminder this event is raised by WPF each time an animation frame is rendered, 30 or 60 times per seconds)

View’s code behind:

public MainWindow()
{
    InitializeComponent();

    DataContext = new ViewModel();

    CompositionTarget.Rendering += OnRendering;
}

void OnRendering(object sender, EventArgs e)
{
    if (DataContext is IRefresh)
        ((IRefresh)DataContext).Refresh();
}

ViewModel:

class ViewModel : INotifyPropertyChanged, IRefresh
{
    public ViewModel()
    {
        model = new Model();
    }

    public void Refresh()
    {
        Progress = model.Progress;
        Frequency = model.Frequency;
    }
    
    // ...the remaining is identical to the original ViewModel
}

You get almost the same result as Solution 1, and even a slightly faster execution speed.

Screen Capture

Conclusion

I like simple solutions, that why I really prefer the last one.

Whether it respects or not the MVVM pattern is really a matter of opinion. I really like the idea of the View being responsible of the timer logic and the ViewModel being responsible of updating its value.

One thing I really appreciate on the polling approach is that it really decouples the Model’s and the ViewModel’s execution threads. We can even get rid of the Model’s events.

To conclude, here is a comparison of the memory consumptions: Task manager

PS: A word about concurrency

When using the polling technique, you should take a special care of the concurrency.

Since the properties of the Model are accessed from several threads, you may need to add lock blocks if the type is bigger than a processor word (in my examples I used a int so that’s OK).

If you have a lot of changing properties in your model, you should group them in a class, like ModelState. That way, the ViewModel will only have one property to monitor and only this class needs to be thread safe.

See the complete project on GitHub

转载

您可以按照以下步骤来实现该教程实例: 1. 首先,确保您已经安装了 WPF(Windows Presentation Foundation)在您的开发环境中。 2. 在 Visual Studio(或其他适用的 IDE)中,创建一个新的 WPF 项目。 3. 打开项目文件夹,并找到主窗口的 XAML 文件(通常命名为 MainWindow.xaml)。将其打开以编辑。 4. 在 XAML 文件中,找到 `<Grid>` 标签,并在其中添加一个新的 `<lv:Chart>` 标签。确保已引用 `xmlns:lv="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"` 命名空间。 5. 在 `<lv:Chart>` 标签中,添加一个 `<lv:CartesianChart>` 标签。设置 `Series` 属性为 `"{Binding Data}"`,其中 Data 是您将提供的数据源。 6. 在 `<lv:Chart>` 标签中,添加一个 `<lv:CartesianChart.AxisX>` 标签和一个 `<lv:CartesianChart.AxisY>` 标签。在这些标签中,您可以设置有关 X 和 Y 轴的属性,例如标题、刻度、范围等。 7. 在代码文件(例如 MainWindow.xaml.cs)中,创建一个名为 Data 的属性,并将其设置为包含您要显示的数据的集合。确保该属性实现了 `INotifyPropertyChanged` 接口,并在集合更改时触发 `PropertyChanged` 事件。 8. 在窗口的构造函数或其他适当的位置,初始化 Data 属性,并将其绑定到 Chart 控件的 Series 属性。 9. 运行您的应用程序,将会看到一个包含指定数据的 XY 折线图。 请注意,以上步骤假设您已经安装了 LiveCharts 库,并在项目中引用了适当的程序集。如果您尚未安装或引用该库,您可以从 https://www.lvcharts.net/ 下载并安装它。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值