Repository: AvaloniaUI/Avalonia (2026-03-22T21:50:20Z)

Issue:
PropertyChanged no longer works
### Describe the bug

Calling `OnPropertyChanged` to update the UI so it matches the underlying property no longer works.

**Xaml:**
```
<CheckBox Content="Bound to IsChecked"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        IsChecked="{Binding IsChecked, Mode=OneWay}" />
<TextBlock Text="{Binding IsChecked, StringFormat='Actual IsChecked value: {0}'}" />
<Button Content="Raise property changed"
        Command="{Binding RaisePropertyChangedCommand}" />
```

**VM:**
```
private bool _isChecked;
public bool IsChecked
{
        get => _isChecked;
        set
        {
                _isChecked = value;
                OnPropertyChanged();
        }
}
public RelayCommand RaisePropertyChangedCommand { get; }

public MainWindowViewModel()
{
        RaisePropertyChangedCommand = new RelayCommand(ExecuteRaisePropertyChanged);
}

private void ExecuteRaisePropertyChanged()
{
        OnPropertyChanged(nameof(IsChecked));
}
```

### To Reproduce

1. Run the [test repository](https://github.com/mohdimas/AvaloniaBindingTest/tree/main)
2. Check the checkbox so the UI is checked. But as it's one-way binding, so the underlying property is not updated
3. Click the button to call `OnropertyChanged`.
4. The expected behavior should update the UI to match the underlying property but it doesnt work

![Kapture 2024-12-03 at 11 19 34](https://github.com/user-attachments/assets/f371a564-e68d-4e8e-93be-45b81de4c99a)


### Expected behavior

On Avalonia 11.0.13 it still works ([TestRepo](https://github.com/mohdimas/AvaloniaBindingTest/tree/avalonia-11.0.13))

![Kapture 2024-12-03 at 11 12 41](https://github.com/user-attachments/assets/50f4c9da-5c3d-477f-bf61-59720e64352c)


### Avalonia version

11.1.0 and above including latest nightly build

### OS

macOS

### Additional context

_No response_


Discussion:
I can repro on nightly (11.3.999-cibuild0053693-alpha), so this seems to be a breaking change in the 11.1 binding refactor. 

`PropertyChanged` seems to be squashed when the source value hasn't changed from the prior `PropertyChanged` event. Perhaps an optimization for most scenarios as `OneWay` is typically used for control properties that the control itself does not mutate?

Repro works as expected when you propagate hard changes. ie.
```cs
private void ExecuteRaisePropertyChanged()
{
    IsChecked ^= true;
    IsChecked ^= true;
}
```
[This is the line which blocks the update.](https://github.com/AvaloniaUI/Avalonia/blob/e452c85695e6cfca13a7d069a17ebf687294d7cb/src/Avalonia.Base/PropertyStore/ValueStore.cs?plain=1#L810C1-L810C37)



Patch:
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index 81a21955c4f..281e9f6b614 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -205,7 +205,9 @@ internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataVa
                 var error = dataValidationError is not null ?
                     new BindingError(dataValidationError, BindingErrorType.DataValidationError) :
                     null;
-                ConvertAndPublishValue(value, error);
+
+                var forceUpdate = _mode == BindingMode.OneWay;
+                ConvertAndPublishValue(value, error, forceUpdate);
             }
         }
         else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null)
@@ -402,7 +404,7 @@ private void Log(AvaloniaObject target, string error, string errorPoint, LogEven
             error);
     }
 
-    private void ConvertAndPublishValue(object? value, BindingError? error)
+    private void ConvertAndPublishValue(object? value, BindingError? error, bool forceUpdate = false)
     {
         var isTargetNullValue = false;
 
@@ -450,7 +452,7 @@ private void ConvertAndPublishValue(object? value, BindingError? error)
             value = ConvertFallback(FallbackValue, nameof(FallbackValue));
 
         // Publish the value.
-        PublishValue(value, error);
+        PublishValue(value, error, forceUpdate);
     }
 
     private void WriteTargetValueToSource()
diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
index e45c88c4578..874c3c2e137 100644
--- a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
+++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
@@ -406,7 +406,8 @@ protected void Log(AvaloniaObject target, string error, LogEventLevel level = Lo
     /// </summary>
     /// <param name="value">The new value, or <see cref="UnchangedValue"/>.</param>
     /// <param name="error">The new binding or data validation error.</param>
-    private protected void PublishValue(object? value, BindingError? error = null)
+    /// <param name="forceUpdate">If true, forces the value to be published even if it hasn't changed.</param>
+    private protected void PublishValue(object? value, BindingError? error = null, bool forceUpdate = false)
     {
         Debug.Assert(value is not BindingNotification);
         Debug.Assert(value != BindingOperations.DoNothing);
@@ -424,7 +425,7 @@ private protected void PublishValue(object? value, BindingError? error = null)
             value = null;
         }
 
-        var hasValueChanged = value != UnchangedValue && !TypeUtilities.IdentityEquals(value, GetValue(), TargetType);
+        var hasValueChanged = forceUpdate || (value != UnchangedValue && !TypeUtilities.IdentityEquals(value, GetValue(), TargetType));
         var hasErrorChanged = error is not null || _error is not null;
 
         if (hasValueChanged)


Test patch:
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs
index cf80b863860..c933eb5316c 100644
--- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs
@@ -1,3 +1,5 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
 using Avalonia.Data;
 using Xunit;
 
@@ -201,4 +203,23 @@ public void Can_Bind_Readonly_Property_OneWayToSource()
 
         Assert.Equal("foo", data.StringValue);
     }
+
+    [Fact]
+    public void OneWay_Binding_Updates_Target_When_Changes_And_Source_Raises_PropertyChanged()
+    {
+        var data = new ViewModel { StringValue = "foo" };
+        var target = CreateTarget<ViewModel, string?>(
+            x => x.StringValue,
+            dataContext: data,
+            mode: BindingMode.OneWay);
+
+        Assert.Equal("foo", target.String);
+
+        target.SetCurrentValue(TargetClass.StringProperty, "bar");
+        Assert.Equal("bar", target.String);
+
+        data.RaisePropertyChanged(nameof(data.StringValue));
+
+        Assert.Equal("foo", target.String);
+    }
 }