Skip to content

InvalidCastException when clicking on Button where Command is defined in generic parent DataContext #19112

Open
@ghostelet

Description

@ghostelet

Describe the bug

An InvalidCastException is thrown in desktop applications (at least) when clicking on a Button in a view which command is bound to an ICommand defined on the abstract generic parent of its view-model (DataContext):

System.InvalidCastException: 'Unable to cast object of type 'AvaloniaApplication1.Views.ViewModelB' to type 'AvaloniaApplication1.Views.BaseVM[AvaloniaApplication1.Views.ModelA]'.`

Unhandled exception. System.InvalidCastException: Unable to cast object of type 'AvaloniaApplication1.Views.ViewModelB' to type 'AvaloniaApplication1.Views.BaseVM`1[AvaloniaApplication1.Views.ModelA]'.
   at CompiledAvaloniaXaml.XamlIlTrampolines.AvaloniaApplication1:AvaloniaApplication1.Views.BaseVM`1+Save_0!CommandExecuteTrampoline(Object, Object)
   at Avalonia.Data.Core.ExpressionNodes.MethodCommandNode.Command.Execute(Object parameter)
   at Avalonia.Controls.Button.OnClick()
   at Avalonia.Controls.Button.OnPointerReleased(PointerReleasedEventArgs e)
   at Avalonia.Input.InputElement.<>c.<.cctor>b__32_9(InputElement x, PointerReleasedEventArgs e)
   at Avalonia.Interactivity.RoutedEvent`1.<>c__DisplayClass1_0`1.<AddClassHandler>g__Adapter|0(Object sender, RoutedEventArgs e)
   at Avalonia.Interactivity.RoutedEvent.<>c__DisplayClass23_0.<AddClassHandler>b__0(ValueTuple`2 args)
   at Avalonia.Reactive.AnonymousObserver`1.OnNext(T value)
   at Avalonia.Reactive.LightweightObservableBase`1.PublishNext(T value)
   at Avalonia.Reactive.LightweightSubject`1.OnNext(T value)
   at Avalonia.Interactivity.RoutedEvent.InvokeRaised(Object sender, RoutedEventArgs e)
   at Avalonia.Interactivity.EventRoute.RaiseEventImpl(RoutedEventArgs e)
   at Avalonia.Interactivity.EventRoute.RaiseEvent(Interactive source, RoutedEventArgs e)
   at Avalonia.Interactivity.Interactive.RaiseEvent(RoutedEventArgs e)
   at Avalonia.Input.MouseDevice.MouseUp(IMouseDevice device, UInt64 timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers, IInputElement hitTest)
   at Avalonia.Input.MouseDevice.ProcessRawEvent(RawPointerEventArgs e)
   at Avalonia.Input.MouseDevice.ProcessRawEvent(RawInputEventArgs e)
   at Avalonia.Input.InputManager.ProcessInput(RawInputEventArgs e)
   at Avalonia.Controls.TopLevel.<>c.<HandleInput>b__150_0(Object state)
   at Avalonia.Threading.Dispatcher.Send(SendOrPostCallback action, Object arg, Nullable`1 priority)
   at Avalonia.Controls.TopLevel.HandleInput(RawInputEventArgs e)
   at Avalonia.Win32.WindowImpl.AppWndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.WindowImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.WindowImpl.WndProcMessageHandler(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.Interop.UnmanagedMethods.DispatchMessage(MSG& lpmsg)
   at Avalonia.Win32.Win32DispatcherImpl.RunLoop(CancellationToken cancellationToken)
   at Avalonia.Threading.DispatcherFrame.Run(IControlledDispatcherImpl impl)
   at Avalonia.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at Avalonia.Threading.Dispatcher.MainLoop(CancellationToken cancellationToken)
   at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.StartCore(String[] args)
   at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(String[] args)
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder builder, String[] args, Action`1 lifetimeBuilder)
   at AvaloniaApplication1.Desktop.Program.Main(String[] args) in X:\XXX\AvaloniaApplication1\AvaloniaApplication1.Desktop\Program.cs:line 12

To Reproduce

Have the following setup (a working example is attached to this ticket):

  • A base abstract generic view-model (BaseVM<TModel>)
  • 2 concrete view-models inheriting the aforementioned view-model (for example ViewModelA : BaseVM<ModelA> and ViewModelB : BaseVM<ModelB>
  • A TemplatedControl (say CustomControl) exposing an ICommand as a StyledProperty.
  • A MainView referencing two instances of the CustomControl each bound to one of the two concrete view-models defined above.

When you run the desktop application, clicking on the first button will work fine. Clicking on the second one will lead to the exception. If somehow you comment out the code for the first view-model, the second button will work. Looks like it will only work with the first view-model as per alphabetical order.

internal abstract class BaseVM<TModel> : ViewModelBase
{
    public void Save()
    {
        Console.Write("crashes even before being called!");
    }
}

internal sealed class ModelA
{
}

internal sealed class ModelB
{ 
}

internal sealed class ViewModelA : BaseVM<ModelA>
{
}

internal sealed class ViewModelB : BaseVM<ModelB>
{
}

The Custom Control code:

public class CustomControl : TemplatedControl
{
    public static readonly StyledProperty<ICommand?> SaveCommandProperty =
        StyledProperty<ICommand?>.Register<CustomControl, ICommand?>(
            nameof(SaveCommand),
            null,
            false,
            BindingMode.TwoWay);
    
    public ICommand? SaveCommand
    {
        get => this.GetValue(SaveCommandProperty);
        set => this.SetValue(SaveCommandProperty, value);
    }
}

Its XAML:

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="using:AvaloniaApplication1.Views">
    <Design.PreviewWith>
        <controls:CustomControl />
    </Design.PreviewWith>

    <Style Selector="controls|CustomControl">
        <!-- Set Defaults -->
        <Setter Property="Template">
            <ControlTemplate>
                <Button Content="Click Me!" Command="{TemplateBinding SaveCommand}" />
            </ControlTemplate>
        </Setter>
    </Style>
</Styles>

And replace the content of the default MainView.xaml proposed in a new Avalonia application with:

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        
        <StackPanel Grid.Row="0" DataContext="{Binding ViewModelA}">
            <views:CustomControl SaveCommand="{Binding Save}" />
        </StackPanel>
        
        <StackPanel Grid.Row="1" DataContext="{Binding ViewModelB}">
            <views:CustomControl SaveCommand="{Binding Save}" />
        </StackPanel>
    </Grid>

Finally, modify the default MainViewModel.cs as such:

internal partial class MainViewModel : ViewModelBase
{
    [ObservableProperty] private ViewModelA viewModelA;

    [ObservableProperty] private ViewModelB viewModelB;

    public MainViewModel()
    {
        this.ViewModelA = new ViewModelA();
        this.ViewModelB = new ViewModelB();
    }
}

AvaloniaApplication1.zip

Expected behavior

The second Button should work the same way as the first one.

Avalonia version

11.3.2

OS

Windows

Additional context

I'm using the CommunityToolkit MVVM.

I managed to reproduce the issue with both Visual Studio and Rider.

Making the Save method in the base view-model virtual and overriding it in at least one of the two children view-models prevents the issue from occurring.
So a workaround - although not very clean - could be to override it in both children and call base.Save(); in both of them

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions