Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regression in 11.1: MultiValueConverter returning BindingNotification throws InvalidCastException #16070

Closed
stogle opened this issue Jun 19, 2024 · 4 comments · Fixed by #16102

Comments

@stogle
Copy link
Contributor

stogle commented Jun 19, 2024

Describe the bug

In my application I have several IMultiValueConverters that return a BindingNotification when values can't be converted. The documentation suggests this is a best practice.

With 11.0.10 I have no issues, however when I tried 11.1.0-rc1, I started to get InvalidCastExceptions like the following when navigating through the application:

System.InvalidCastException: Unable to cast object of type 'MultiBindingBug.ViewModels.MainWindowViewModel' to type 'MultiBindingBug.ViewModels.MultiConverterViewModel'.
   at CompiledAvaloniaXaml.XamlIlHelpers.MultiBindingBug.ViewModels.MultiConverterViewModel,MultiBindingBug.Welcome!Getter(Object)
   at Avalonia.Data.Core.ClrPropertyInfo.Get(Object target)
   at Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.InpcPropertyAccessor.get_Value()
   at Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.InpcPropertyAccessor.SendCurrentValue()

In the debugger, stepping up through the stack, I see that in PropertyInfoAccessorFactory.SendCurrentValue a different exception is being caught:

System.InvalidCastException: Unable to cast object of type 'Avalonia.Data.BindingNotification' to type 'System.String'.
   at Avalonia.PropertyStore.EffectiveValue`1.SetLocalValueAndRaise(ValueStore owner, AvaloniaProperty property, Object value)
   at Avalonia.PropertyStore.ValueStore.SetLocalValue(AvaloniaProperty property, Object value)
   at Avalonia.PropertyStore.ValueStore.Avalonia.Data.Core.IBindingExpressionSink.OnChanged(UntypedBindingExpressionBase instance, Boolean hasValueChanged, Boolean hasErrorChanged, Object value, BindingError error)
   at Avalonia.Data.Core.UntypedBindingExpressionBase.PublishValue(Object value, BindingError error)
   at Avalonia.Data.Core.UntypedObservableBindingExpression.System.IObserver<System.Object>.OnNext(Object value)
   at Avalonia.Reactive.Observable.<>c__DisplayClass4_1`1.<Where>b__1(TSource input)
   at Avalonia.Reactive.AnonymousObserver`1.OnNext(T value)
   at Avalonia.Reactive.Observable.<>c__DisplayClass2_1`2.<Select>b__1(TSource input)
   at Avalonia.Reactive.AnonymousObserver`1.OnNext(T value)
   at Avalonia.Reactive.Operators.Sink`1.ForwardOnNext(TTarget value)
   at Avalonia.Reactive.Operators.CombineLatest`2._.OnNext(Int32 index, TSource value)
   at Avalonia.Reactive.Operators.CombineLatest`2._.SourceObserver.OnNext(TSource value)
   at Avalonia.Reactive.LightweightObservableBase`1.PublishNext(T value)
   at Avalonia.Data.Core.UntypedBindingExpressionBase.ObservableSink.PublishNext(Object value)
   at Avalonia.Data.Core.UntypedBindingExpressionBase.ObservableSink.Avalonia.Data.Core.IBindingExpressionSink.OnChanged(UntypedBindingExpressionBase instance, Boolean hasValueChanged, Boolean hasErrorChanged, Object value, BindingError error)
   at Avalonia.Data.Core.UntypedBindingExpressionBase.PublishValue(Object value, BindingError error)
   at Avalonia.Data.Core.BindingExpression.ConvertAndPublishValue(Object value, BindingError error)
   at Avalonia.Data.Core.BindingExpression.OnNodeError(Int32 nodeIndex, String error)
   at Avalonia.Data.Core.BindingExpression.OnNodeValueChanged(Int32 nodeIndex, Object value, Exception dataValidationError)
   at Avalonia.Data.Core.ExpressionNodes.ExpressionNode.SetValue(Object value, Exception dataValidationError)
   at Avalonia.Data.Core.ExpressionNodes.ExpressionNode.SetValue(Object valueOrNotification)
   at Avalonia.Data.Core.ExpressionNodes.DataContextNode.OnPropertyChanged(Object sender, AvaloniaPropertyChangedEventArgs e)
   at Avalonia.AvaloniaObject.RaisePropertyChanged[T](AvaloniaProperty`1 property, Optional`1 oldValue, BindingValue`1 newValue, BindingPriority priority, Boolean isEffectiveValue)
   at Avalonia.PropertyStore.EffectiveValue`1.RaiseInheritedValueChanged(AvaloniaObject owner, AvaloniaProperty property, EffectiveValue oldValue, EffectiveValue newValue)
   at Avalonia.PropertyStore.ValueStore.InheritedValueChanged(AvaloniaProperty property, EffectiveValue oldValue, EffectiveValue newValue)
   at Avalonia.PropertyStore.ValueStore.InheritedValueChanged(AvaloniaProperty property, EffectiveValue oldValue, EffectiveValue newValue)
   at Avalonia.PropertyStore.ValueStore.InheritedValueChanged(AvaloniaProperty property, EffectiveValue oldValue, EffectiveValue newValue)
   at Avalonia.PropertyStore.ValueStore.InheritedValueChanged(AvaloniaProperty property, EffectiveValue oldValue, EffectiveValue newValue)
   at Avalonia.PropertyStore.ValueStore.SetInheritanceParent(AvaloniaObject newParent)
   at Avalonia.AvaloniaObject.set_InheritanceParent(AvaloniaObject value)
   at Avalonia.StyledElement.Avalonia.Controls.ISetLogicalParent.SetParent(ILogical parent)
   at Avalonia.StyledElement.ClearLogicalParent(IList children)
   at Avalonia.StyledElement.LogicalChildrenCollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
   at Avalonia.Visual.LogicalChildrenCollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
   at Avalonia.Collections.AvaloniaList`1.NotifyRemove(T item, Int32 index)
   at Avalonia.Collections.AvaloniaList`1.Remove(T item)
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild(Object content)
   at Avalonia.Controls.Presenters.ContentPresenter.ContentChanged(AvaloniaPropertyChangedEventArgs e)
   at Avalonia.Controls.Presenters.ContentPresenter.OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.AvaloniaObject.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.Animation.Animatable.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.AvaloniaObject.RaisePropertyChanged[T](AvaloniaProperty`1 property, Optional`1 oldValue, BindingValue`1 newValue, BindingPriority priority, Boolean isEffectiveValue)
   at Avalonia.PropertyStore.EffectiveValue`1.SetAndRaiseCore(ValueStore owner, StyledProperty`1 property, T value, BindingPriority priority, Boolean isOverriddenCurrentValue, Boolean isCoercedDefaultValue)
   at Avalonia.PropertyStore.EffectiveValue`1.SetAndRaise(ValueStore owner, IValueEntry value, BindingPriority priority)
   at Avalonia.PropertyStore.ValueStore.ReevaluateEffectiveValue(AvaloniaProperty property, EffectiveValue current, IValueEntry changedValueEntry, Boolean ignoreLocalValue)
   at Avalonia.PropertyStore.ValueStore.Avalonia.Data.Core.IBindingExpressionSink.OnChanged(UntypedBindingExpressionBase instance, Boolean hasValueChanged, Boolean hasErrorChanged, Object value, BindingError error)
   at Avalonia.Data.Core.UntypedBindingExpressionBase.PublishValue(Object value, BindingError error)
   at Avalonia.Data.TemplateBinding.PublishValue()
   at Avalonia.Data.TemplateBinding.OnTemplatedParentPropertyChanged(Object sender, AvaloniaPropertyChangedEventArgs e)
   at Avalonia.AvaloniaObject.RaisePropertyChanged[T](AvaloniaProperty`1 property, Optional`1 oldValue, BindingValue`1 newValue, BindingPriority priority, Boolean isEffectiveValue)
   at Avalonia.PropertyStore.EffectiveValue`1.SetAndRaiseCore(ValueStore owner, StyledProperty`1 property, T value, BindingPriority priority, Boolean isOverriddenCurrentValue, Boolean isCoercedDefaultValue)
   at Avalonia.PropertyStore.EffectiveValue`1.SetLocalValueAndRaise(ValueStore owner, StyledProperty`1 property, T value)
   at Avalonia.PropertyStore.EffectiveValue`1.SetLocalValueAndRaise(ValueStore owner, AvaloniaProperty property, Object value)
   at Avalonia.PropertyStore.ValueStore.SetLocalValue(AvaloniaProperty property, Object value)
   at Avalonia.PropertyStore.ValueStore.Avalonia.Data.Core.IBindingExpressionSink.OnChanged(UntypedBindingExpressionBase instance, Boolean hasValueChanged, Boolean hasErrorChanged, Object value, BindingError error)
   at Avalonia.Data.Core.UntypedBindingExpressionBase.PublishValue(Object value, BindingError error)
   at Avalonia.Data.Core.BindingExpression.ConvertAndPublishValue(Object value, BindingError error)
   at Avalonia.Data.Core.BindingExpression.OnNodeValueChanged(Int32 nodeIndex, Object value, Exception dataValidationError)
   at Avalonia.Data.Core.ExpressionNodes.ExpressionNode.SetValue(Object value, Exception dataValidationError)
   at Avalonia.Data.Core.ExpressionNodes.ExpressionNode.SetValue(Object valueOrNotification)
   at Avalonia.Data.Core.ExpressionNodes.PropertyAccessorNode.OnValueChanged(Object newValue)
   at Avalonia.Data.Core.Plugins.PropertyAccessorBase.PublishValue(Object value)
   at Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.InpcPropertyAccessor.SendCurrentValue()

This is what lead me to the conclusion that the problem was with the converter returning a BindingNotification.

To Reproduce

In my application, the exception occurs when navigating between views and seems to be triggered by changes in DataContext. I have tried to create a minimal repro:

MainWindow.xaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:MultiBindingBug.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:views="clr-namespace:MultiBindingBug.Views"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="MultiBindingBug.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Title="MultiBindingBug">
    <Grid ColumnDefinitions="Auto,*">
        <StackPanel Grid.Column="0">
            <Button Command="{Binding HomeCommand}">Home</Button>
            <Button Command="{Binding MultiConverterCommand}">MultiConverter</Button>
        </StackPanel>

        <ContentControl Content="{Binding SelectedContent}"
                        Margin="0,0,2,0"
                        Grid.Column="1">
            <ContentControl.DataTemplates>
                <DataTemplate DataType="vm:HomeViewModel">
                    <views:HomeView />
                </DataTemplate>
                <DataTemplate DataType="vm:MultiConverterViewModel">
                    <views:MultiConverterView />
                </DataTemplate>
            </ContentControl.DataTemplates>
        </ContentControl>
    </Grid>
</Window>
HomeView.xaml
<UserControl xmlns="https://github.com/avaloniaui"
             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"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="MultiBindingBug.Views.HomeView">
    Home
</UserControl>
MultiConverter.xaml
<UserControl xmlns="https://github.com/avaloniaui"
             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:vm="using:MultiBindingBug.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="MultiBindingBug.Views.MultiConverterView"
             x:DataType="vm:MultiConverterViewModel">
    <StackPanel>
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock.Text>
                <MultiBinding Converter="{x:Static vm:MultiValueConverter.Instance}" ConverterParameter=" ">
                    <Binding Path="Welcome" />
                    <Binding Path="To" />
                    <Binding Path="Avalonia" />
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </StackPanel>
</UserControl>
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MultiBindingBug.ViewModels;

public partial class MainWindowViewModel : ObservableObject
{
    private readonly HomeViewModel _home = new HomeViewModel();
    private readonly MultiConverterViewModel _multiConverter = new MultiConverterViewModel();

    public MainWindowViewModel() => SelectedContent = _home;

    [RelayCommand] public void Home() => SelectedContent = _home;
    [RelayCommand] public void MultiConverter() => SelectedContent = _multiConverter;

    [ObservableProperty] private object? _selectedContent;
}
HomeViewModel.cs
namespace MultiBindingBug.ViewModels;

public class HomeViewModel
{
}
MultiConverterViewModel.cs
namespace MultiBindingBug.ViewModels;

public class MultiConverterViewModel
{
    public string Welcome => "Welcome";
    public string To => "to";
    public string Avalonia => "Avalonia!";
}
MultiValueConverter.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Data;
using Avalonia.Data.Converters;

namespace MultiBindingBug.ViewModels;

public class MultiValueConverter : IMultiValueConverter
{
    public static MultiValueConverter Instance { get; } = new();

    public object Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
    {
        if (values.All(v => v is string) && parameter is string separator)
        {
            return string.Join(separator, values.Cast<string>());
        }
        
        return new BindingNotification(new ArgumentException(), BindingErrorType.Error);
    }
}

Steps to reproduce:

  1. Run the application. HomeView is displayed.
  2. Click on the "MultiConverter" button to switch to MultiConverterView.
  3. Click on the "Home" button. The exception is thrown.

Expected behavior

No exception is thrown. (If I'm doing something wrong in my MultiValueConverter please advise.)

Avalonia version

11.1.0-rc1

OS

Windows

Additional context

Workarounds:

  1. Downgrade to Avalonia 11.0.10
  2. Modify MultiValueConverter.cs to return null instead of a BindingNotification
@grokys
Copy link
Member

grokys commented Jun 21, 2024

Definitely looks like a regression introduced by the binding system refactor, will investigate.

Not sure if this is a showstopper and needs to be fixed for 11.1.0 or whether it can wait for 11.1.1 though.

@stogle
Copy link
Contributor Author

stogle commented Jun 21, 2024

Definitely looks like a regression introduced by the binding system refactor, will investigate.

Not sure if this is a showstopper and needs to be fixed for 11.0.0 or whether it can wait for 11.0.1 though.

Assuming you mean 11.1.0 and 11.1.1?

For me, I don't use the BindingNotifications my converters were returning, so I could return null. I'm also not in a big hurry to move to 11.1, so I could wait. I would suggest documenting this as a known issue though, because my application was very broken and it took me a long time to track it down to the BindingNotifcations.

@grokys
Copy link
Member

grokys commented Jun 21, 2024

Assuming you mean 11.1.0 and 11.1.1?

Yeah sorry, edited. I'll take a look and see how hard the fix is before we decide if it'll be in 11.1.0. It definitely feels like less of a showstopper than #16071 though.

grokys added a commit that referenced this issue Jun 24, 2024
github-merge-queue bot pushed a commit that referenced this issue Jun 25, 2024
* Added failing test for #16070.

* Handle BindingNotifications in UntypedObservableBindingExpression.

Fixes #16070
grokys added a commit that referenced this issue Jun 28, 2024
* Added failing test for #16070.

* Handle BindingNotifications in UntypedObservableBindingExpression.

Fixes #16070
@stogle
Copy link
Contributor Author

stogle commented Jul 8, 2024

I've confirmed that this issue is fixed for me in 11.1.0-RC2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants