【问题标题】:WPF TextBlock won't update despite PropertyChanged尽管 PropertyChanged,WPF TextBlock 不会更新
【发布时间】:2021-10-24 06:10:20
【问题描述】:

我正在编写一个 MVVM 应用程序,并试图在底部包含一个状态栏。我已经设置了视图和视图模型,它们应该跟踪应用程序的 Logger 类的状态,它是一个易于使用的单例。

查看:

<UserControl x:Class="SynthEBD.UC_StatusBar"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:SynthEBD"
         mc:Ignorable="d" 
         d:DataContext="{d:DesignInstance Type=local:VM_StatusBar}"
         d:DesignHeight="450" d:DesignWidth="800">
<Grid >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Path=DispString}" Foreground="{Binding Path=FontColor}" FontSize="18" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>

查看模型:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;

namespace SynthEBD
{
    public class VM_StatusBar : INotifyPropertyChanged
    {
        public VM_StatusBar()
        {
            this.DispString = "";
            this.FontColor = new SolidColorBrush(Colors.Green);
            this.SubscribedLogger = Logger.Instance;
            this.SubscribedLogger.PropertyChanged += RefreshDisp;
        }

        public string DispString { get; set; }
        private Logger SubscribedLogger { get; set; }
        public SolidColorBrush FontColor { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;

        public void RefreshDisp(object sender, PropertyChangedEventArgs e)
        {
            this.DispString = SubscribedLogger.StatusString;
            this.FontColor = SubscribedLogger.StatusColor;
        }
    }
}

记录器:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;

namespace SynthEBD
{
    public sealed class Logger : INotifyPropertyChanged
    {
        private static Logger instance;
        private static object lockObj = new Object();

        public event PropertyChangedEventHandler PropertyChanged;

        public VM_RunButton RunButton { get; set; }
        public string StatusString { get; set; }
        public string LogString { get; set; }

        public SolidColorBrush StatusColor { get; set; }

        public SolidColorBrush ReadyColor = new SolidColorBrush(Colors.Green);
        public SolidColorBrush WarningColor = new SolidColorBrush(Colors.Yellow);
        public SolidColorBrush ErrorColor = new SolidColorBrush(Colors.Red);
        public string ReadyString = "Ready To Patch";

        private Logger()
        {
            this.StatusColor = this.ReadyColor;
            this.StatusString = this.ReadyString;
        }

        public static Logger Instance
        {
            get
            {
                lock (lockObj)
                {
                    if (instance == null)
                    {
                        instance = new Logger();
                    }
                }
                return instance;
            }
        }

        public static void LogError(string error)
        {
            Instance.LogString += error + "\n";
        }

        public static void LogErrorWithStatusUpdate(string error, ErrorType type)
        {
            Instance.LogString += error + "\n";
            Instance.StatusString = error;
            switch (type)
            {
                case ErrorType.Warning: Instance.StatusColor = Instance.WarningColor; break;
                case ErrorType.Error: Instance.StatusColor = Instance.ErrorColor; break;
            }
        }

        public static void TimedNotifyStatusUpdate(string error, ErrorType type, int durationSec)
        {
            LogErrorWithStatusUpdate(error, type);

            var t = Task.Factory.StartNew(() =>
            {
                Task.Delay(durationSec * 1000).Wait();
            });
            t.Wait();
            ClearStatusError();
        }
        public static void ClearStatusError()
        {
            Instance.StatusString = Instance.ReadyString;
            Instance.StatusColor = Instance.ReadyColor;
        }
    }

    public enum ErrorType
    {
        Warning,
        Error
    }
}

我有意触发 Logger.TimedNotifyStatusUpdate() 函数,即使我可以看到在 VM_StatusBar.RefreshDisp() 中到达断点,实际屏幕上的字符串和颜色永远不会改变 (https://imgur.com/BhizinR)。我没有看到任何失败的绑定,所以我不明白为什么视图没有更新。感谢您的建议!

编辑:我也尝试显式触发 PropertyChanged 事件,而不是如下依赖 PropertyChanged.Fody,但屏幕上的结果是相同的。

public class VM_StatusBar : INotifyPropertyChanged
    {
        public VM_StatusBar()
        {
            this.DispString = "";
            this.FontColor = new SolidColorBrush(Colors.Green);
            this.SubscribedLogger = Logger.Instance;
            this.SubscribedLogger.PropertyChanged += RefreshDisp;
        }

        public string DispString
        {
            get { return _dispString; }
            set
            {
                if (value != _dispString)
                {
                    _dispString = value;
                    OnPropertyChanged("DispString");
                }
            }
        }
        private string _dispString;
        private Logger SubscribedLogger { get; set; }
        public SolidColorBrush FontColor { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, e);
        }

        protected void OnPropertyChanged(string propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        public void RefreshDisp(object sender, PropertyChangedEventArgs e)
        {
            this.DispString = SubscribedLogger.StatusString;
            this.FontColor = SubscribedLogger.StatusColor;
            string debugBreakHere = "";
        }
    }

【问题讨论】:

  • 仅仅设置属性值是不够的。每当调用 setter 时,您都必须触发 PropertyChanged 事件。
  • 我正在使用 PropertyChanged.Fody,所以我认为这应该为我处理 (github.com/Fody/PropertyChanged)。我的所有其他绑定都可以正确更新,而无需手动触发事件。这个特定的配置有什么需要的吗?
  • 你在 UI 线程上调用 TimedNotifyStatusUpdate 吗?如果它是从不同的线程调用的,属性更改通知将无处可去
  • 如果您明智地测试,删除整个 TaskFactory.StartNew 块会发生什么?这是以同步阻塞方式等待的异步延迟......
  • DataContext是如何设置的?你确定绑定解决了吗?您应该启用数据绑定跟踪并检查调试器的输出窗口是否存在绑定错误。

标签: c# wpf data-binding


【解决方案1】:

您永远不应该在 Task 对象上调用 Task.Wait。始终await 它以允许它异步完成。从您发布的代码看来,您正在阻塞 UI 线程,从而窃取更新表面(渲染)所需的资源。 Task.Wait 是一张通往死锁的门票。
另外,比起Task.Factory,更喜欢Task.Run

将你的阻塞代码变成非阻塞应该可以解决问题:

public static async Task TimedNotifyStatusUpdateAsync(string error, ErrorType type, int durationSec)
{
  LogErrorWithStatusUpdate(error, type);

  Task t = Task.Run(async () =>
  {
    // Calling Wait on the Task blocks this thread
    //Task.Delay(durationSec * 1000).Wait();

    // Instead 'await' the Task to free resources
    await Task.Delay(durationSec * 1000);
  });

  // Await the Task to allow the UI thread to render the view
  // in order to show the changes
  await t;   

  ClearStatusError();
}

然后使用await从另一个async Task方法调用该方法:

private async Task CallTimedNotifyStatusUpdateAsync()
  => await TimedNotifyStatusUpdateAsync();

请注意,将异步方法包装到 Task.Run 中通常不是一个好主意。 TimedNotifyStatusUpdateAsync 的正确实现是:

public static async Task TimedNotifyStatusUpdateAsync(string error, ErrorType type, int durationSec)
{
  LogErrorWithStatusUpdate(error, type);

  // Await the Task to allow the UI thread to render the view
  // in order to show the changes     
  await Task.Delay(durationSec * 1000);

  ClearStatusError();
}

【讨论】:

  • 漂亮,通过如下实现代码使其工作:catch{ Logger.Instance.CallTimedNotifyStatusUpdateAsync("Could not delete file at " + this.SelectedHeightConfig.SourcePath, ErrorType.Warning, 5);}请问为什么在您的示例代码中将 CallTimedNotifyStatusUpdateAsync 设为私有?我必须将其公开,以便可以从调用函数访问它。我是否应该从另一个公开的函数中调用 CallTimedNotifyStatusUpdateAsync 本身?
  • 不,背后没有任何意图。 CallTimedNotifyStatusUpdateAsync 方法是完全组成的。我只是介绍它来展示如何调用修改后的 TimedNotifyStatusUpdateAsync 方法。因此前缀 Call... (CallTimedNotifyStatusUpdateAsync).
猜你喜欢
  • 2014-08-15
  • 2011-07-29
  • 1970-01-01
  • 2012-09-01
  • 2023-03-30
  • 1970-01-01
  • 2011-04-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多