Home > Software design >  WPF TextBlock won't update despite PropertyChanged
WPF TextBlock won't update despite PropertyChanged

Time:10-25

I'm writing an MVVM app and I'm trying to include a status bar at the bottom. I've set up the view and view model, which are supposed to track the status of the app's Logger class, which is a singleton for ease of use.

View:

<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>

View Model:

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;
        }
    }
}

Logger:

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
    }
}

I am intentionally triggering the Logger.TimedNotifyStatusUpdate() function, and even though I can see a breakpoint being reached within VM_StatusBar.RefreshDisp(), the actual on-screen string and color never change (https://imgur.com/BhizinR). I don't see any failed bindings, so I don't understand why the view is not updating. Thanks for any advice!

Edit: I also tried explicitly triggering the PropertyChanged event instead of relying on PropertyChanged.Fody as follows, but the on-screen result was identical.

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 = "";
        }
    }

CodePudding user response:

You should never call Task.Wait on a Task object. Always await it in order to allow it to complete asynchronously. From your posted code it looks like you are blocking the UI thread, thus stealing resources that are needed to update the surface (rendering). Task.Wait is a ticket to a deadlock.
Also, prefer Task.Run over Task.Factory.

Turning your blocking code into non-blocking shoulkd do the trick:

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();
}

Then call the method from another async Task method using await:

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

Note that wrappping an async method into Task.Run is generally not a good idea. The correct implementation of TimedNotifyStatusUpdateAsync would be:

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();
}
  • Related