简体   繁体   English

WPF 有时会忽略 PropertyChanged 事件

[英]WPF sometimes ignores PropertyChanged event

I have a desktop application with a stopwatch-like functionality.我有一个具有类似秒表功能的桌面应用程序。 I want to show the time elapsed since I start tracking time, and update the UI with the elapsed time.我想显示自我开始跟踪时间以来经过的时间,并用经过的时间更新 UI。 I have a background timer that will periodically raise PropertyChanged for the elapsed time, and the UI will update.我有一个后台计时器,它会在经过的时间定期引发PropertyChanged ,并且 UI 会更新。

The problem is the majority of the time the UI does not update.问题是大部分时间 UI 不会更新。 I have the timer on 100ms interval, occasionally it will update several times a second, usually it will update once every 3-5 seconds.我的计时器间隔为 100 毫秒,偶尔它会每秒更新几次,通常每 3-5 秒更新一次。 Sometimes it will be ~25 seconds before the UI updates.有时在 UI 更新之前大约需要 25 秒。 If I drag the window the UI always updates correctly while it is being dragged (I don't have any event handlers for dragging or clicking).如果我拖动窗口,则在拖动窗口时 UI 始终正确更新(我没有任何用于拖动或单击的事件处理程序)。

This is dotnet core 3.1 on windows.这是 Windows 上的 dotnet core 3.1。 Here is the stripped down project.这是精简的项目。

.csproj: .csproj:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DocumentationFile></DocumentationFile>
  </PropertyGroup>

</Project>

MainWindow.xaml主窗口.xaml

<Window x:Class="SkillTracker.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Name="TheWindow"
        Title="Skill Tracker" Height="200" Width="350">
    <Window.Resources>
    </Window.Resources>

    <Grid>
        <StackPanel>
            <TextBlock >Total time:</TextBlock>
            <TextBlock FontFamily="Lucida Console" Text="{Binding TotalTimeElapsed, UpdateSourceTrigger=Explicit}"></TextBlock>
            <TextBlock >Session time:</TextBlock>
            <TextBlock FontFamily="Lucida Console" Text="{Binding CurrentSessionTimeElapsed}"></TextBlock>
            <Button Command="{Binding StartStopArtCommand}" Content="button" ></Button>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs主窗口.xaml.cs

using System;
using System.ComponentModel;
using System.Timers;
using System.Windows;
using System.Windows.Input;

namespace SkillTracker
{
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    public class CommandHandler : ICommand
    {
        private Action _action;
        public event EventHandler CanExecuteChanged;
        public CommandHandler(Action action)
        {
            _action = action;
        }
        public bool CanExecute(object parameter) => true;
        public void Execute(object parameter) => _action();
    }

    public class MainViewModel : ViewModelBase
    {
        private System.Timers.Timer _uiUpdateTimer;
        private DateTime? _sessionStart;
        private int _totalDurationSeconds;

        public MainViewModel()
        {
            _totalDurationSeconds = 3;

            StartStopArtCommand = new CommandHandler(StartStopArtCommandAction);

            _uiUpdateTimer = new System.Timers.Timer();
            _uiUpdateTimer.Interval = 100;
            _uiUpdateTimer.AutoReset = true;
            _uiUpdateTimer.Elapsed += UiUpdateTimer_Elapsed;
        }

        public ICommand StartStopArtCommand { get; private set; }

        public string CurrentSessionTimeElapsed
        {
            get
            {
                if (_uiUpdateTimer.Enabled && _sessionStart.HasValue)
                {
                    var ts = DateTime.Now - _sessionStart.Value;
                    var hours = (ts.Days * 24) + ts.Hours;
                    return $"{hours:000}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}";
                }
                else
                {
                    return string.Empty;
                }
            }
        }

        public string TotalTimeElapsed
        {
            get
            {
                if (_sessionStart.HasValue)
                {
                    var ts = DateTime.Now.AddSeconds(_totalDurationSeconds) - _sessionStart.Value;
                    var hours = (ts.Days * 24) + ts.Hours;
                    return $"{hours:000}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}";
                }
                else
                {
                    return string.Empty;
                }
            }
        }

        private void StartStopArtCommandAction()
        {
            _sessionStart = DateTime.Now;
            _uiUpdateTimer.Start();

            // trigger initial update.
            OnPropertyChanged(nameof(TotalTimeElapsed));
            OnPropertyChanged(nameof(CurrentSessionTimeElapsed));
        }

        private void UiUpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.ffff"));
            OnPropertyChanged(nameof(CurrentSessionTimeElapsed));
            OnPropertyChanged(nameof(TotalTimeElapsed));
        }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new MainViewModel();
        }
    }
}

Here's an animated gif of what not-updating looks like.这是未更新外观的动画 gif。 You can see the UiUpdateTimer_Elapsed method being called, it's printing the current DateTime in the debug window on the right.您可以看到正在调用UiUpdateTimer_Elapsed方法,它在右侧的调试窗口中打印当前的 DateTime。

Like the comments mentioned switching to a DispatcherTimer will give you better results as it's Tick event fires on the UI thread.就像提到的评论一样,切换到 DispatcherTimer 会给你更好的结果,因为它的 Tick 事件在 UI 线程上触发。 A System.Timer runs in a different thread pool and can cause issues. System.Timer 在不同的线程池中运行,可能会导致问题。

Regardless, your sample will run a little more roughly in debug mode, because you are writing to the Output window on each tick.无论如何,您的示例将在调试模式下运行得更粗略一些,因为您在每个滴答声中都在写入输出窗口。

To implement the DispatcherTimer update your code with these changes:要实现 DispatcherTimer,请使用以下更改更新您的代码:

private DispatcherTimer _uiUpdateTimer;

...

_uiUpdateTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
_uiUpdateTimer.Tick += OnTick;

...

if(_uiUpdateTimer.IsEnabled && _sessionState.HasValue)
{
    ... Your code goes here
}

...

private void OnTick(object sender, EventArgs e)
{
    ... Your code goes here
}

I hope this helps.我希望这有帮助。

Using a System.Windows.Threading.DispatcherTimer by itself wasn't enough, I also had to specify a high priority (in the constructor) for the UI to update like it should. System.Windows.Threading.DispatcherTimer使用System.Windows.Threading.DispatcherTimer是不够的,我还必须为 UI 指定高优先级(在构造函数中)以使其更新。

_uiUpdateTimer = new DispatcherTimer(DispatcherPriority.Normal);
_uiUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);
_uiUpdateTimer.Tick += UiUpdateTimer_Elapsed;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM