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.63k stars 2.22k forks source link

Event handlers won't work when using DataTemplates #2527

Closed worldbeater closed 5 years ago

worldbeater commented 5 years ago

While struggling to prevent the ListBox from handling right mouse button clicks which are intended to be used for showing ContextMenus and not selecting anything in the ListBox, I discovered that event handlers won't work when referencing the handler method from a control declared inside a DataTemplate.

The following setup doesn't work

<ListBox>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Grid PointerReleased="OnPointerReleased" />
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

and

public void OnPointerReleased(object sender, PointerReleasedEventArgs args) { }

will result into PotableXaml exception:

Unhandled Exception: Portable.Xaml.XamlObjectWriterException: 
  Could not convert object 'OnPointerReleased' (of type System.String) to {clr-namespace:System;assembly=mscorlib}EventHandler({clr-namespace:Avalonia.Input;assembly=Avalonia.Input}PointerReleasedEventArgs): 
  Referenced value method OnPointerReleased in type Avalonia.Controls.Grid indicated by event System.EventHandler`

the new XamlIl compiler throws the following at runtime, much more descriptive btw:

Unhandled Exception: System.ArgumentException: Delegate to an instance method cannot have null 'this'.
   at System.MulticastDelegate.ThrowNullThisInDelegateToInstance()
   at System.MulticastDelegate.CtorClosed(Object target, IntPtr methodPtr)
   at Camelotia.Presentation.Avalonia.Views.ProviderView.XamlIlClosure_cdf814c6-3b50-4b3f-a642-f88f39d4942a.Build(IServiceProvider ) in Camelotia.Presentation.Avalonia.Views.ProviderView.xaml:line 52
   at Avalonia.Markup.Xaml.Templates.TemplateContent.Load(Object templateContent) in D:\a\1\s\src\Markup\Avalonia.Markup.Xaml\Templates\TemplateContent.cs:line 43
   at Avalonia.Controls.Presenters.ContentPresenter.CreateChild() in D:\a\1\s\src\Avalonia.Controls\Presenters\ContentPresenter.cs:line 327
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild() in D:\a\1\s\src\Avalonia.Controls\Presenters\ContentPresenter.cs:line 224
   at Avalonia.Controls.Mixins.ContentControlMixin.<>c__DisplayClass1_0`1.<Attach>g__TemplateApplied|1(Object s, RoutedEventArgs ev) in D:\a\1\s\src\Avalonia.Controls\Mixins\ContentControlMixin.cs:line 75
--- End of stack trace from previous location where exception was thrown ---
   at Avalonia.Interactivity.RoutedEvent.<>c__DisplayClass25_0.<AddClassHandler>b__0(Tuple`2 args) in D:\a\1\s\src\Avalonia.Interactivity\RoutedEvent.cs:line 115
   at System.Reactive.Subjects.Subject`1.OnNext(T value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Subjects\Subject.cs:line 148
   at Avalonia.Interactivity.Interactive.RaiseEventImpl(RoutedEventArgs e) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 188
   at Avalonia.Interactivity.Interactive.RaiseEvent(RoutedEventArgs e) in D:\a\1\s\src\Avalonia.Interactivity\Interactive.cs:line 129
   at Avalonia.Controls.Primitives.TemplatedControl.ApplyTemplate() in D:\a\1\s\src\Avalonia.Controls\Primitives\TemplatedControl.cs:line 271
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 511
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Controls.VirtualizingStackPanel.UpdateAdd(IControl child) in D:\a\1\s\src\Avalonia.Controls\VirtualizingStackPanel.cs:line 206
   at Avalonia.Controls.VirtualizingStackPanel.ChildrenChanged(Object sender, NotifyCollectionChangedEventArgs e) in D:\a\1\s\src\Avalonia.Controls\VirtualizingStackPanel.cs:line 116
   at Avalonia.Collections.AvaloniaList`1.NotifyAdd(IList t, Int32 index) in D:\a\1\s\src\Avalonia.Base\Collections\AvaloniaList.cs:line 505
   at Avalonia.Controls.Presenters.ItemVirtualizerSimple.CreateAndRemoveContainers() in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemVirtualizerSimple.cs:line 342
   at Avalonia.Controls.Presenters.ItemVirtualizerSimple.UpdateControls() in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemVirtualizerSimple.cs:line 163
   at Avalonia.Controls.VirtualizingStackPanel.MeasureOverride(Size availableSize) in D:\a\1\s\src\Avalonia.Controls\VirtualizingStackPanel.cs:line 93
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Controls.Presenters.ItemVirtualizerSimple.MeasureOverride(Size availableSize) in D:\a\1\s\src\Avalonia.Controls\Presenters\ItemVirtualizerSimple.cs:line 151
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Controls.Presenters.ScrollContentPresenter.MeasureOverride(Size availableSize) in D:\a\1\s\src\Avalonia.Controls\Presenters\ScrollContentPresenter.cs:line 210
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Controls.Grid.<>c__DisplayClass32_0.<MeasureOverride>g__MeasureOnce|2(Control child, Size size) in D:\a\1\s\src\Avalonia.Controls\Grid.cs:line 341
   at Avalonia.Controls.Grid.<>c__DisplayClass32_0.<MeasureOverride>b__0(Control child) in D:\a\1\s\src\Avalonia.Controls\Grid.cs:line 301
   at Avalonia.Controls.Utils.GridLayout.AppendMeasureConventions[T](IDictionary`2 source, Func`2 getDesiredLength) in D:\a\1\s\src\Avalonia.Controls\Utils\GridLayout.cs:line 133
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint) in D:\a\1\s\src\Avalonia.Controls\Grid.cs:line 302
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Layout.Layoutable.MeasureOverride(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 565
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding) in D:\a\1\s\src\Avalonia.Layout\LayoutHelper.cs:line 44
   at Avalonia.Controls.Border.MeasureOverride(Size availableSize) in D:\a\1\s\src\Avalonia.Controls\Border.cs:line 106
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Layout.Layoutable.MeasureOverride(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 565
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint) in D:\a\1\s\src\Avalonia.Controls\Grid.cs:line 280
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 516
   at Avalonia.Layout.Layoutable.Measure(Size availableSize) in D:\a\1\s\src\Avalonia.Layout\Layoutable.cs:line 317
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 177
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 166
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 166
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 166
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 166
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control) in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 166
   at Avalonia.Layout.LayoutManager.ExecuteMeasurePass() in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 128
   at Avalonia.Layout.LayoutManager.ExecuteLayoutPass() in D:\a\1\s\src\Avalonia.Layout\LayoutManager.cs:line 90
   at Avalonia.Threading.JobRunner.RunJobs(Nullable`1 priority) in D:\a\1\s\src\Avalonia.Base\Threading\JobRunner.cs:line 40
   at Avalonia.X11.X11PlatformThreading.HandleX11(CancellationToken cancellationToken)
   at Avalonia.X11.X11PlatformThreading.RunLoop(CancellationToken cancellationToken) in D:\a\1\s\src\Avalonia.X11\X11PlatformThreading.cs:line 200
   at Avalonia.Threading.Dispatcher.MainLoop(CancellationToken cancellationToken) in D:\a\1\s\src\Avalonia.Base\Threading\Dispatcher.cs:line 65
   at Avalonia.Application.Run(Window mainWindow) in D:\a\1\s\src\Avalonia.Controls\Application.cs:line 237

The following setup works

<Grid PointerReleased="OnPointerReleased" />

and

public void OnPointerReleased(object sender, PointerReleasedEventArgs args) { } 

will result into a working application triggering the OnPointerReleased event handler.

Repro

The repro can be found here.

worldbeater commented 5 years ago

A temporary solution is to extract the DataTemplate into a separate UserControl, and then embed it into the DataTemplate. Generally, this approach is a lot more MVVMish than using event handlers, so a rule of thumb is to always create user controls and view models for them when working with DataTemplates. If you need to bind to the DataContext (view model) of a parent control, then a wise decision would be to pass a reference to the parent view model down to children view models.

For example, on the parent control side it could look like as follows:

<ListBox SelectionMode="Toggle" Items="{Binding Files}">
  <ListBox.ItemTemplate>
    <DataTemplate DataType="interfaces:IFileViewModel">
      <views:FileView DataContext="{Binding}" />
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

And on the child control side it would be:

<UserControl xmlns="https://github.com/avaloniaui"
             x:Class="Camelotia.Presentation.Avalonia.Views.FileView"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             PointerReleased="OnPointerReleased">
    <TextBlock Text="{Binding Name}" />
</UserControl>

With the following code-behind:

public sealed class FileView : ReactiveUserControl<IFileViewModel>
{
    public FileView()
    {
        AvaloniaXamlLoader.Load(this);
        this.WhenActivated(disposables =>
        {
            Observable // Using Observables and ReactiveUI is possible as well
                 .FromEventPattern<PointerReleasedEventHandler, PointerReleasedEventArgs>(
                    handler => PointerReleased += handler,
                    handler => PointerReleased -= handler)
                .Subscribe(args => Console.WriteLine("It also works with observables!"))
                .DisposeWith(disposables);
        });
    }

    // This works fine and throws no exceptions!
    public void OnPointerReleased(object sender, PointerReleasedEventArgs args)
    {
        Console.WriteLine("It works with events!")
    } 
}
worldbeater commented 5 years ago

Feels like this one is a good old issue! 😅

grokys commented 5 years ago

@kekekeks should this be already supported on master now the XAML compiler is merged? Just tried it and got a runtime exception:

System.ArgumentException
  HResult=0x80070057
  Message=Delegate to an instance method cannot have null 'this'.
  Source=System.Private.CoreLib
  StackTrace:
   at System.MulticastDelegate.ThrowNullThisInDelegateToInstance()
   at System.MulticastDelegate.CtorClosed(Object target, IntPtr methodPtr)
   at ControlCatalog.Pages.ListBoxPage.XamlIlClosure_4e8d2b91-7fe5-409e-855a-35e26bb05209.Build(IServiceProvider ) in D:\projects\AvaloniaUI\Avalonia\samples\ControlCatalog\Pages/ListBoxPage.xaml:line 15
   at Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers.<>c__DisplayClass0_0.<DeferredTransformationFactoryV1>b__0(IServiceProvider sp) in D:\projects\AvaloniaUI\Avalonia\src\Markup\Avalonia.Markup.Xaml\XamlIl\Runtime\XamlIlRuntimeHelpers.cs:line 21
   at Avalonia.Markup.Xaml.Templates.TemplateContent.Load(Object templateContent) in D:\projects\AvaloniaUI\Avalonia\src\Markup\Avalonia.Markup.Xaml\Templates\TemplateContent.cs:line 43
   at Avalonia.Markup.Xaml.Templates.DataTemplate.Build(Object data) in D:\projects\AvaloniaUI\Avalonia\src\Markup\Avalonia.Markup.Xaml\Templates\DataTemplate.cs:line 35
   at Avalonia.Controls.Presenters.ContentPresenter.CreateChild() in D:\projects\AvaloniaUI\Avalonia\src\Avalonia.Controls\Presenters\ContentPresenter.cs:line 327
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild() in D:\projects\AvaloniaUI\Avalonia\src\Avalonia.Controls\Presenters\ContentPresenter.cs:line 224
   at Avalonia.Controls.Presenters.ContentPresenter.ApplyTemplate() in D:\projects\AvaloniaUI\Avalonia\src\Avalonia.Controls\Presenters\ContentPresenter.cs:line 205
   at Avalonia.Controls.Mixins.ContentControlMixin.<>c__DisplayClass1_0`1.<Attach>g__TemplateApplied|1(Object s, RoutedEventArgs ev) in D:\projects\AvaloniaUI\Avalonia\src\Avalonia.Controls\Mixins\ContentControlMixin.cs:line 73
kekekeks commented 5 years ago

Will investigate

balthild commented 4 years ago

In v0.8.3, event handlers don't work even if there's no DataTemplates in XAML.

The project is created by Visual Studio, with Avalonia MVVM Application template.

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:AvaloniaApplication1.ViewModels;assembly=AvaloniaApplication1"
        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="AvaloniaApplication1.Views.MainWindow"
        Icon="/Assets/avalonia-logo.ico"
        Title="AvaloniaApplication1">

    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <StackPanel>
        <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Button HorizontalAlignment="Center" VerticalAlignment="Center" PointerReleased="TestOnPointerReleased">Test</Button>
    </StackPanel>

</Window>
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace AvaloniaApplication1.Views {
    public class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
#if DEBUG
            this.AttachDevTools();
#endif
        }

        private void InitializeComponent() {
            AvaloniaXamlLoader.Load(this);
        }

        public void TestOnPointerReleased(object sender) {
            System.Console.WriteLine("asdf");
        }
    }
}

These code results in the runtime exception:

Portable.Xaml.XamlObjectWriterException
  HResult=0x80131500
  Message=Could not convert object 'TestOnPointerReleased' (of type System.String) to {clr-namespace:System;assembly=mscorlib}EventHandler({clr-namespace:Avalonia.Input;assembly=Avalonia.Input}PointerReleasedEventArgs): Referenced value method TestOnPointerReleased in type AvaloniaApplication1.Views.MainWindow indicated by event System.EventHandler`1[[Avalonia.Input.PointerReleasedEventArgs, Avalonia.Input, Version=0.8.0.0, Culture=neutral, PublicKeyToken=null]] was not found
  Source=Avalonia.Markup.Xaml
  StackTrace:
   at Portable.Xaml.XamlObjectWriterInternal.GetCorrectlyTypedValue(XamlMember xm, XamlType xt, Object value, Boolean fallbackToString)
   at Portable.Xaml.XamlObjectWriterInternal.StoreAppropriatelyTypedValue(ObjectState state, MemberAndValue ms, Object obj, Object keyObj)
   at Portable.Xaml.XamlObjectWriterInternal.OnWriteValue(Object value)
   at Portable.Xaml.XamlServices.Transform(XamlReader xamlReader, XamlWriter xamlWriter, Boolean closeWriter)
   at Avalonia.Markup.Xaml.AvaloniaXamlLoader.LoadFromReader(XamlReader reader, AvaloniaXamlContext context, IAmbientProvider parentAmbientProvider)
   at Avalonia.Markup.Xaml.AvaloniaXamlLoader.Load(Stream stream, Assembly localAssembly, Object rootInstance, Uri uri)
   at Avalonia.Markup.Xaml.AvaloniaXamlLoader.Load(Type type, Object rootInstance)
   at Avalonia.Markup.Xaml.AvaloniaXamlLoader.Load(Object obj)
   at AvaloniaApplication1.Views.MainWindow.InitializeComponent() in C:\Users\Balthild\source\repos\AvaloniaApplication1\AvaloniaApplication1\Views\MainWindow.xaml.cs:line 15
   at AvaloniaApplication1.Views.MainWindow..ctor() in C:\Users\Balthild\source\repos\AvaloniaApplication1\AvaloniaApplication1\Views\MainWindow.xaml.cs:line 8
   at AvaloniaApplication1.Program.AppMain(Application app, String[] args) in C:\Users\Balthild\source\repos\AvaloniaApplication1\AvaloniaApplication1\Program.cs:line 24
   at AvaloniaApplication1.Program.Main(String[] args) in C:\Users\Balthild\source\repos\AvaloniaApplication1\AvaloniaApplication1\Program.cs:line 12
XamlObjectWriterException: Referenced value method TestOnPointerReleased in type AvaloniaApplication1.Views.MainWindow indicated by event System.EventHandler`1[[Avalonia.Input.PointerReleasedEventArgs, Avalonia.Input, Version=0.8.0.0, Culture=neutral, PublicKeyToken=null]] was not found
grokys commented 4 years ago

@balthild it's fixed in the 0.9 previews - you can get the previews from NuGet if you check "Include prerelease": https://www.nuget.org/packages/Avalonia/0.9.0-preview4

balthild commented 4 years ago

@grokys Thanks!