AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.97k stars 2.25k forks source link

InvalidCastException when using generic parent's function as button command #13825

Open ruafelianna opened 11 months ago

ruafelianna commented 11 months ago

Describe the bug

Hello! I have a base generic class and it's two heirs. Base class has a function, which I bind to buttons as a command for both of my heirs. When I click on the button which is bound to the first heir everything is fine. When I click on the button which is bound to the second heir I get InvalidCastException. I am not sure if that's a bug or if I am doing something very wrong. I hope for your help, cheers!

To Reproduce

I've created a fresh project to reproduce this behaviour. Here is the code:

Models/Models.cs

internal class BaseModel<TData, TSelf>(Func<TData> print)
    where TSelf : BaseModel<TData, TSelf>
{
    public void Print()
    {
        Console.WriteLine(_print());
    }

    private readonly Func<TData> _print = print;
}

internal class ModelA(Func<string> print) : BaseModel<string, ModelA>(print)
{
}

internal class ModelB(Func<byte> print) : BaseModel<byte, ModelB>(print)
{
}

ViewModels/MainWindowViewModel.cs

internal class MainWindowViewModel : ViewModelBase
{
    public ModelA ModelA => new(() => "apple");
    public ModelB ModelB => new(() => 123);
}

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:TestAvaloniaBug.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" Width="300" Height="200"
        x:Class="TestAvaloniaBug.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        Title="TestAvaloniaBug">

    <StackPanel>
        <Button Content="Print A" Command="{Binding ModelA.Print}"/>

        <Button Content="Print B" Command="{Binding ModelB.Print}"/>
    </StackPanel>

</Window>

Expected behaviour

Press button "Print A" -> see message "apple". Press button "Print B" -> see message "123".

Actual behaviour

Press button "Print A" -> see message "apple". Press button "Print B" -> get exception:

System.InvalidCastException
  HResult=0x80004002
  Message=Unable to cast object of type 'TestAvaloniaBug.Models.ModelB' to type 'TestAvaloniaBug.Models.BaseModel`2[System.String,TestAvaloniaBug.Models.ModelA]'.
  Source=TestAvaloniaBug
  StackTrace:
   at CompiledAvaloniaXaml.XamlIlTrampolines.TestAvaloniaBug:TestAvaloniaBug.Models.BaseModel`2+Print_0!CommandExecuteTrampoline(Object , Object )
   at Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CommandAccessorPlugin.CommandAccessor.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.Reactive.LightweightObservableBase`1.PublishNext(T value)
   at Avalonia.Interactivity.EventRoute.RaiseEventImpl(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.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.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.Start(String[] args)
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder builder, String[] args, ShutdownMode shutdownMode)
   at TestAvaloniaBug.Program.Main(String[] args) in C:\...\TestAvaloniaBug\Program.cs:line 13

Environment

P.S.

I have a temporary solution to the issue: everything is fine when I override the function in any of the heirs. For instance:

internal class ModelA(Func<string> print) : BaseModel<string, ModelA>(print)
{
    public new void Print() => base.Print();
}

Update 2023-12-06

CompiledAvaloniaXaml

using Avalonia.Utilities;
using System.Globalization;
using System.Runtime.InteropServices;
using TestAvaloniaBug.Models;

namespace CompiledAvaloniaXaml
{
  internal class XamlIlTrampolines
  {
    public static void TestAvaloniaBug:TestAvaloniaBug.Models.BaseModel`2+Print_0!CommandExecuteTrampoline(
      [In] object obj0,
      [In] object obj1)
    {
      ((BaseModel<string, ModelA>) obj0).Print();
    }
  }
}
ChrisAvg commented 11 months ago

Hi, are you sure is it only C#?

ruafelianna commented 11 months ago

@ChrisAvg, hello, I am not quite sure I understand what you mean.

ChrisAvg commented 11 months ago

Which language do you use?

ruafelianna commented 11 months ago

@ChrisAvg C# 12

maxkatz6 commented 4 months ago

@ruafelianna it might be fixed in 11.1 rc builds