Description
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>
andViewModelB : BaseVM<ModelB>
- A
TemplatedControl
(sayCustomControl
) exposing anICommand
as aStyledProperty
. - A
MainView
referencing two instances of theCustomControl
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();
}
}
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