-1

Problem:

Style gets set when the window is first loaded, then I am unable to replace it.

XAML:

Style="{Binding Path=BoolProperty, Converter={StaticResource ButtonStyleConverter}}"

VM:

Property:

public bool BoolProperty => SomeOtherObject.YetAnotherObject.OtherBoolProperty;

In the constructor:

SomeOtherObject.YetAnotherObject.PropertyChanged += (s, a)
                    => PropertyChanged.Notify(this, nameof(BoolProperty));

Converter:

  • Converter returns a Style object based on the BoolProperty
  • Not including code because it works as expected, see Solution.

Styles:

  • It has been proven that the styles are correct and are not causing the issue in this case (see Solution)

Notes:

  • Notify() is just an extension method to ease use of the IPropertyChanged
  • When calling the same Notify() after the root event and all the notifies, converter calls etc., the style updates correctly

I verified the following:

  • When the event causing the change happens, PropertyChanged.Notify is called correctly
  • Then the getter of the BoolProperty is called as expected
  • Right after that the converter is called and I verified that it returns the correct style
  • When inspecting the style in Live Property Explorer it clearly looks like the 1st style is still set

What I've tried:

  • Changing .Notify(this, nameof(BoolProperty)) to .NotifyAll(this)
  • Applying the second style first (to see if it's not an issue with the style itself)
  • Adding UpdateSourceTrigger=PropertyChanged}
  • Replacing Path=BoolProperty with just BoolProperty
  • Adding ConverterParameter attribute with the property name

Solution/Workaround:

Thanks to @EldHasp I was able to verify that it's in fact not XAMl/Converter/Style issue but it's related to how Notify() calls are made. I don't know why UI doesn't update when all the calls/threads finish, but I fixed it by replacing:

SomeOtherObject.YetAnotherObject.PropertyChanged += (s, a)
                    => PropertyChanged.Notify(this, nameof(BoolProperty));

With:

this.Command_That_Also_Relies_On_OtherBoolProperty.CanExecuteChanged += (s, a) 
    => PropertyChanged.Notify(this, nameof(BoolProperty));

While a hack, this workaround is acceptable in my case as I have no time to further investigate the root cause. For completion, the command looks as follows:

public ICommand SomeCommand_That_Also_Relies_On_YetAnotherObject => new RelayCommand(
            () =>  /* some code */ ,
            () => SomeOtherObject.YetAnotherObject.OtherBoolProperty);

The command also requires the following to refresh:

CommandManager.InvalidateRequerySuggested();

It looks like the issue was that the Notify() was not called from the Main Thread.

Actual solution:

Raising OnPropertyChanged when an object property is modified from another thread

Camile
  • 124
  • 9
  • From your description, it is most likely that the problem lies in the XAML where the binding is set. Is the converter only used in one place? – EldHasp Apr 28 '21 at 06:27
  • @EldHasp I use it in two different places. It applies the style based on converter the first time, but when it's supposed to change (converter returns a different style), the change is not reflected in the UI. – Camile Apr 28 '21 at 06:40

1 Answers1

1

The problem is clearly not in the code you showed. Here's the simplest example that works great.

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace StyleBindingConverter
{
    public class BooleanToStyleConverter : IValueConverter
    {
        public Style TrueStyle { get; set; }
        public Style FalseStyle { get; set; }

        private static readonly BooleanConverter boolConverter = new BooleanConverter();
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (!(value is bool boolean))
            {
                string str = value?.ToString();
                if (string.IsNullOrWhiteSpace(str) ||
                    !boolConverter.IsValid(str))
                    return DependencyProperty.UnsetValue;

                boolean = (bool)boolConverter.ConvertFromString(value.ToString());
            }


            return boolean
                ? TrueStyle
                : FalseStyle;

        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
namespace StyleBindingConverter
{
    public class BooleanViewModel
    {
        public bool BoolProperty { get; set; }
    }
}
<Window x:Class="StyleBindingConverter.TestStyleConverterWindow"
        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:local="clr-namespace:StyleBindingConverter"
        mc:Ignorable="d"
        Title="TestStyleConverterWindow" Height="450" Width="400">
    <Window.Resources>
        <Style x:Key="Button.Style.True" TargetType="Button">
            <Setter Property="Background" Value="Green"/>
        </Style>
        <Style x:Key="Button.Style.False" TargetType="Button">
            <Setter Property="Background" Value="LightCoral"/>
        </Style>
        <local:BooleanToStyleConverter x:Key="BooleanToStyleConverter"
                                       FalseStyle="{StaticResource Button.Style.False}"
                                       TrueStyle="{StaticResource Button.Style.True}"/>
    </Window.Resources>
    <Window.DataContext>
        <local:BooleanViewModel/>
    </Window.DataContext>
    <UniformGrid Columns="1">
        <Button Content="Test Button" HorizontalAlignment="Center" VerticalAlignment="Center"
                Padding="15 5"
                Style="{Binding BoolProperty, Converter={StaticResource BooleanToStyleConverter}, Mode=OneWay}"/>
        <CheckBox Content="Style Change" HorizontalAlignment="Center" VerticalAlignment="Center"
                IsChecked="{Binding BoolProperty, Mode=TwoWay}"/>
    </UniformGrid>
</Window>

Asynchrony has absolutely no effect on the behavior of properties, notifications, and the converter.

Here's an example of changing a property asynchronously. The base implementation of INotifyPropertyChanged from the topic is used: BaseInpc.

using Simplified;
using System.Timers;

namespace StyleBindingConverter
{
    public class BooleanViewModelAsync : BaseInpc
    {
        private bool _boolProperty;

        public bool BoolProperty { get => _boolProperty; private set => Set(ref _boolProperty, value); }

        private readonly Timer timer = new Timer() { Interval = 1000};

        public BooleanViewModelAsync()
        {
            timer.Elapsed += (s, e) => BoolProperty = !BoolProperty;
            timer.Start();
        }
    }

}
<Window x:Class="StyleBindingConverter.TestStyleConverterAsyncWindow"
        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:local="clr-namespace:StyleBindingConverter"
        mc:Ignorable="d"
        Title="TestStyleConverterAsyncWindow"
        Height="450" Width="400">
    <Window.Resources>
        <Style x:Key="Button.Style.True" TargetType="Button">
            <Setter Property="Background" Value="Green"/>
        </Style>
        <Style x:Key="Button.Style.False" TargetType="Button">
            <Setter Property="Background" Value="LightCoral"/>
        </Style>
        <local:BooleanToStyleConverter x:Key="BooleanToStyleConverter"
                                       FalseStyle="{StaticResource Button.Style.False}"
                                       TrueStyle="{StaticResource Button.Style.True}"/>
    </Window.Resources>
    <Window.DataContext>
        <local:BooleanViewModelAsync/>
    </Window.DataContext>
    <Grid>
        <Button Content="Test Button" HorizontalAlignment="Center" VerticalAlignment="Center"
                Padding="15 5"
                Style="{Binding BoolProperty, Converter={StaticResource BooleanToStyleConverter}, Mode=OneWay}"/>
    </Grid>
</Window>
EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • Fyi you were right - I wasn't able to reproduce the problem using your code and upon further investigation it seems like the PropertyChanged is fired too early (doesn't seem to be). If I call it again after some time in some other place in the code it changes the style correctly. I'll continue investigating on my own, thank you. – Camile Apr 28 '21 at 08:49
  • A typical call is `PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));`. But I do not think that this is the problem, since you write that your converter is executed. I hope you checked this way: a breakpoint on the `Convert (...)` method, every time the `BoolProperty` changes, a stop occurs and then step by step (F10) until the method exits. – EldHasp Apr 28 '21 at 09:40
  • I think I found the issue - it looks like it was because the event starting the chain of notifications was on a different thread. See the updated question. When subscribing to my command's CanExecuteChanged instead, I was able to fix the issue and confirmed, that then it runs from the Main Thread rather than from some other thread. – Camile Apr 28 '21 at 10:00
  • For the PropertyChanged event, it doesn't matter which thread it is fired on. But the CollectionChanged and CanExecuteChanged events are thread-sensitive. If they are not created in the UI thread, they can "disappear" or even throw an exception and crash the application. – EldHasp Apr 28 '21 at 10:12
  • Some additional details: https://stackoverflow.com/questions/66951283/in-wpf-is-the-dependency-object-subcribing-the-notifypropertychanged-event-once/66952236#66952236 – EldHasp Apr 28 '21 at 10:14
  • So why would subscribing to command's CanExecuteChanged suddenly work? It uses the very same property and subscribes to the very same object's PropertyChanged as the original solution, with the only exception being that RequerySuggested is called (otherwise UI wouldn't refresh based on the command either). – Camile Apr 28 '21 at 10:22
  • Read my explanation here: https://stackoverflow.com/questions/19183841/why-is-collectionchanged-not-threadsafe/67241294#67241294 – EldHasp Apr 28 '21 at 10:30
  • Finally found the solution - [capturing the SynchronizationContext looks to be the right way to do it](https://stackoverflow.com/questions/35901454/raising-onpropertychanged-when-an-object-property-is-modified-from-another-threa). Thank you for all the help and guiding me to the answer. – Camile Apr 28 '21 at 10:57
  • Asynchrony has absolutely no effect on the behavior of properties, notifications, and the converter. The answer is supplemented. – EldHasp Apr 28 '21 at 11:34
  • Synchronization is needed for the CanExecuteChanged and CollectionChanged events. There may also be differences in implementation for different platforms. My answer is targeting WPF Windows Desktop. – EldHasp Apr 28 '21 at 11:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/231698/discussion-between-camile-and-eldhasp). – Camile Apr 28 '21 at 12:35