【问题标题】:DependencyProperty ignoring some of PropertyChanged callsDependencyProperty 忽略了一些 PropertyChanged 调用
【发布时间】:2015-11-26 13:35:34
【问题描述】:

我的 DependencyProperty 有问题。假设您有一个更新某些 UI 元素的计时器,如果每 100 毫秒调用一次回调,这反过来又更新 UI,那么我没有问题,但是,如果计时器设置为 ~10 毫秒,例如,一些调用将得到忽略。我做了一个重现问题的小解决方案:

这是一个具有依赖属性的自定义 UIElement:

public class CustomLabel : Label
{
    public float Range
    {
        get { return (float)GetValue(MaxRangeProperty); }
        set { SetValue(MaxRangeProperty, value); }
    }

    public static readonly DependencyProperty MaxRangeProperty =
        DependencyProperty.Register("Range", typeof(float), typeof(CustomLabel),
            new PropertyMetadata(0f, RangePropertyChanged));

    private static void RangePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var self = d as CustomLabel;
        Debug.WriteLine("CustomLabel");

        self.Content = self.Range;
    }
}

这是一个 ViewModel,它触发一个计时器并更新一个属性,该属性又应该调用 CustomLabel 上的 DependencyProperty 上的回调。

public class ViewModel : INotifyPropertyChanged
{
    Timer timer;
    Thread t;

    public ViewModel()
    {
        t = new Thread(() => timer = new Timer(new TimerCallback(CallBack), null, 0, 10));
        t.Start();
        Range = 100;
    }

    void CallBack(object state)
    {
        Range = (new Random()).Next(0, 1000);
    }

    private float _range;
    public float Range
    {
        get { return _range; }
        set
        {
            if (_range != value)
            {
                _range = value;
                NotifyPropertyChanged();
                Debug.WriteLine("ViewModel");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

这是 CustomLabel 和 ViewModel 所在的视图:

<Window x:Class="TimerTest.MainWindow"
        xmlns:local="clr-namespace:TimerTest"
        Title="MainWindow">
    <Grid>
        <local:CustomLabel x:Name="customLabel" Range="{Binding Range}"/>
    </Grid>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        ViewModel = new ViewModel();
        customLabel.DataContext = ViewModel;
    }

    public ViewModel ViewModel { get; set; }
}

所以,我在 DependencyProperty 的每一侧都做了一些 Debug.WriteLine() 语句,输出如下所示:

  100ms          10ms
CustomLabel      ViewModel        
ViewModel        CustomLabel
CustomLabel      ViewModel        
ViewModel        ViewModel        
CustomLabel      CustomLabel
ViewModel        ViewModel        
CustomLabel      ViewModel        
ViewModel        ViewModel        
CustomLabel      ViewModel 
ViewModel        CustomLabel

为什么会发生这种情况,我该怎么办? 谢谢你的时间。

【问题讨论】:

  • WPF 每 16.6 毫秒渲染一次屏幕,那么为什么要每 10 毫秒更新一次呢?
  • @GlenThomas 知道这一点很有趣 - 但是我的问题是为什么突然 DependencyProperty 没有收到事件。但是为了回答你的问题,我想尽快更新一个值。我真的不在乎 WPF 何时决定更新,但当它更新时,它必须是最新的值。
  • .net 计时器不过是精确的,您可以在给定的任何间隔上添加 30 毫秒。
  • @eranotzap 您可以在最后一个代码块中清楚地看到,计时器被调用(无论何时)但 DependencyProperty 没有相应地更新 - 这是这个问题的重点。即使计时器是 30 毫秒“太慢”,无论如何都应该调用 DependencyProperty 上的回调。

标签: c# wpf binding dependency-properties


【解决方案1】:

NotifyPropertyChanged 事件由使用队列的Dispatcher 处理。调度程序处理事件的速度比它们添加到队列中的速度慢。

使用DispatcherTimer 可以让您更快地更新:

DispatcherTimer timer =
    new DispatcherTimer(TimeSpan.FromMilliseconds(10),
                    DispatcherPriority.Normal,
                    delegate
                    {
                        MyCustomLabel.SetValue(MaxRangeProperty, viewModel.Range);
                    },
                    Dispatcher);

还有……

默认情况下,您使用的 System.Threading.Timer 类的精度不能达到 10 毫秒。它将使用操作系统计时器。

引用微软关于计时器分辨率的文档:

Windows 7 上的默认计时器分辨率为 15.6 毫秒 (ms)

可以通过调用 Windows API 来增加计时器的分辨率,但这会导致电池消耗。

【讨论】:

  • 我很抱歉格伦 - 这不是问题的答案。或者至少,如果是的话,我错过了一些东西。即使计时器没有 10 毫秒的精度(假设它实际上每 1000 毫秒调用一次 CallBack),它仍应更新 UIElement CustomLabel 上的值。但是,如果您查看最后一个代码块,您可以看到,Timer 被称为 Debug.WriteLine("ViewModel");,但 Debug.WriteLine("CustomLabel"); 没有。
  • 啊 - 我现在明白了,经过您的编辑,我明白了。非常感谢格伦。
  • 我认为您可以通过使用 DispatcherTimer 在 10 毫秒内强制更新。我将添加一个示例。
  • 感谢您的示例,但这对我没有帮助,因为值是通过 DependencyProperty 绑定更新的。但是你回答了我的问题,所以现在我只需要找到一种方法来解决它。