Search code examples
c#.netxamlwinui-3uno-platform

Are `Interaction.Behaviors` supposed to wok in WinUI 3 DataTemplated objects?


On Uno Platform project, using several data templates for TabViewItem, TreeViewItem and ListViewItem, trying to fire commands via various events, like ItemInvoked (TreeView), DoubleTapped (ListView), CloseRequested (TabView).

Strangely enough it works for few invocations, then it stops. Note that (also the same) commands bound to Buttons via their Command binding continue working.

Example of TabView close attempt. Typically works for first 3 to 5 tabs:

        <DataTemplate x:Key="HtmlFileTemplate" x:DataType="local:FileContentViewModel">
            <TabViewItem Header="{x:Bind Info.Name}">
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{x:Bind Content}" />
                    <!--
                        This just works:
                    -->
                    <Button Command="{x:Bind CloseCommand}">Invoke FileContentViewModel.CloseCommand</Button>
                </StackPanel>
                <!-- 
                    This stops working after few invocations
                    (typically together with all other Interaction.Behaviors bindings):
                -->
                <i:Interaction.Behaviors>
                    <ic:EventTriggerBehavior EventName="CloseRequested">
                        <ic:InvokeCommandAction Command="{x:Bind CloseCommand}" />
                    </ic:EventTriggerBehavior>
                </i:Interaction.Behaviors>
            </TabViewItem>
        </DataTemplate>

        <local:TabsTemplateSelector x:Key="TabsTemplateSelector" HtmlFileTemplate="{StaticResource HtmlFileTemplate}" ... />
    ...

    <TabView TabItemsSource="{x:Bind ViewModel.Tabs}" TabItemTemplateSelector="{StaticResource TabsTemplateSelector}">
    </TabView>


I hope, there is some flagrant issue in my usage I can't simply see. Any Help appreciated. Using latest uno stuff (WinUI, dotnet6), debugging on Windows head:

  • dotnet new unoapp -o UnoWinUI3AppName
  • <PackageReference Include="CommunityToolkit.Mvvm" Version="7.1.2" />
  • <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.3" />
  • <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
  • <PackageReference Include="Uno.Microsoft.Xaml.Behaviors.Interactivity.WinUI" Version="2.3.0" />
  • <PackageReference Include="Uno.Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.3.0" />

Still trying to find minimal sample exhibiting the mentioned issues.

Also tried to check the XAML generated code, but it is too much... well... generated :-(

EDIT:

"Simplified" the use case to following:

  • Have TabView on a page.
  • First tab contains ListView with items.
  • Other tabs contain "opened" items (dummy record).
  • Item opens on double-click (DoubleTapped) in ListView on first page.
  • Item closes on X click on a tab (in TabView).
  • Both TabView and ListView use DataTemplates.
  • Separate UI from code as much as possible (binding, commands, VMs, etc.).
  • Based on default WinUI "Hello World" app template.
  • Project files

EDIT 2: For anyone interested, based on Andrew KeepCoding's hints and linked issue answers, I mixed code behind with custom attaching of the behaviors to the templated items via new attached property:

public static class InteractionEx
{
    public static readonly DependencyProperty AttachBehaviorsProperty = DependencyProperty.RegisterAttached
    (
        "AttachBehaviors",
        typeof(object),
        typeof(FrameworkElement),
        new PropertyMetadata(false, AttachBehaviorsChanged)
    );

    public static object GetAttachBehaviors(DependencyObject o) => o.GetValue(AttachBehaviorsProperty);

    public static void SetAttachBehaviors(DependencyObject o, object value) => o.SetValue(AttachBehaviorsProperty, value);


    private static void AttachBehaviorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviors = e.NewValue switch
        {
            Behavior single => new BehaviorCollection { single },
            BehaviorCollection multiple => multiple,
            _ => null,
        };

        Interaction.SetBehaviors(d, behaviors);
    }
}

In XAML the behaviors then can be attached to a control like:

<ListView ItemsSource="{x:Bind Items}" ItemTemplateSelector="{StaticResource ListItemTemplateSelector}" SelectedItem="{x:Bind SelectedItem, Mode=TwoWay}">
    <local:InteractionEx.AttachBehaviors>
        <interactivity:BehaviorCollection>
            <core:EventTriggerBehavior EventName="DoubleTapped">
                <core:InvokeCommandAction Command="{x:Bind OpenCommand}" />
                <core:CallMethodAction MethodName="Open" TargetObject="{x:Bind}" />
            </core:EventTriggerBehavior>
        </interactivity:BehaviorCollection>
    </local:InteractionEx.AttachBehaviors>
</ListView>



Solution

  • I think you'll find these answers helpful.

    But let me suggest another option. I have spent some time with your repro project and in my opinion, I think it'd be cleaner and readable if you drop Interaction.Behaviors. Code-behind is not evil if it's UI related and no business logic there.

    MainWindow.xaml

    <Window
        x:Class="TabViewTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:helpers="using:TabViewTest.Helpers"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewmodels="using:TabViewTest.ViewModels"
        mc:Ignorable="d">
    
        <Grid>
            <Grid.Resources>
                <DataTemplate x:Key="BrowserTemplate" x:DataType="viewmodels:BrowserTabViewModel">
                    <TabViewItem Header="Browser">
                        <ListView x:Name="ItemList" ItemsSource="{x:Bind Items}">
                            <ListView.ItemTemplate>
                                <DataTemplate x:DataType="viewmodels:ItemViewModel">
                                    <TextBlock DoubleTapped="TextBlock_DoubleTapped" Text="{x:Bind Id}" />
                                </DataTemplate>
                            </ListView.ItemTemplate>
                        </ListView>
                    </TabViewItem>
                </DataTemplate>
    
                <DataTemplate x:Key="ContentTemplate" x:DataType="viewmodels:ContentTabViewModel">
                    <TabViewItem Header="{x:Bind ItemViewModel.Id}">
                        <TextBlock Text="{x:Bind ItemViewModel.Id}" />
                    </TabViewItem>
                </DataTemplate>
    
                <helpers:TabItemTemplateSelector
                    x:Key="TabTemplateSelector"
                    BrowserTemplate="{StaticResource BrowserTemplate}"
                    ContentTemplate="{StaticResource ContentTemplate}" />
            </Grid.Resources>
    
            <TabView
                TabCloseRequested="TabView_TabCloseRequested"
                TabItemTemplateSelector="{StaticResource TabTemplateSelector}"
                TabItemsSource="{x:Bind ViewModel.Tabs}" />
        </Grid>
    
    </Window>
    

    MainWindow.xaml.cs

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System.Collections.ObjectModel;
    using System.Linq;
    
    namespace TabViewTest.ViewModels;
    
    [ObservableObject]
    public partial class MainWindowViewModel
    {
        [ObservableProperty]
        private ObservableCollection<TabViewModel> tabs = new();
    
        private readonly BrowserTabViewModel browserTabViewModel;
    
        public MainWindowViewModel()
        {
            browserTabViewModel = new();
            Tabs.Add(browserTabViewModel);
        }
    
        [RelayCommand]
        private void NewTabRequest(ItemViewModel itemViewModel)
        {
            Tabs.Add(new ContentTabViewModel(itemViewModel));
        }
    
        [RelayCommand]
        private void CloseTabRequest(ItemViewModel itemViewModel)
        {
            if (Tabs
                .OfType<ContentTabViewModel>()
                .Where(x => x.ItemViewModel == itemViewModel)
                .FirstOrDefault() is ContentTabViewModel target)
            {
                Tabs.Remove(target);
            }
        }
    }