amwx / FluentAvalonia

Control library focused on fluent design and bringing more WinUI controls into Avalonia
MIT License
985 stars 93 forks source link

TabView - unable to re-order TabItems with a custom Header #498

Closed rasta-mouse closed 9 months ago

rasta-mouse commented 9 months ago

I have quite a basic TabView, which looks something like this:

<ui:TabView IsAddTabButtonVisible="False"
            TabCloseRequested="TabView_TabCloseRequested"
            CanDragTabs="True" CanReorderTabs="True"
            TabItems="{Binding TabView}"
            SelectedItem="{Binding SelectedTab}">
    <ui:TabView.TabItemTemplate>
        <DataTemplate DataType="vm:TabItem">
            <ui:TabViewItem Header="{Binding Header}"
                            Content="{Binding Content}"
                            IconSource="{Binding IconSource}" />
        </DataTemplate>
    </ui:TabView.TabItemTemplate>
</ui:TabView>

Instead of a simple string as my Header, I'm using a custom UserControl in an effort to show more dynamic content. My TabItem class looks like this:

public sealed class TabItem
{
    public IconSource IconSource { get; set; }
    public Control Header { get; set; }
    public Control Content { get; set; }
}

The header renders as intended and you can switch between tabs just fine. However, the application will crash if you try to re-order them. The full stack trace is:

Unhandled exception. System.InvalidOperationException: The control already has a visual parent.
   at Avalonia.Visual.ValidateVisualChild(Visual c)
   at Avalonia.Collections.AvaloniaList`1.Add(T item)
   at Avalonia.Controls.Presenters.ContentPresenter.UpdateChild(Object content)
   at Avalonia.Controls.Presenters.ContentPresenter.ContentChanged(AvaloniaPropertyChangedEventArgs e)
   at Avalonia.Animation.Animatable.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.AvaloniaObject.RaisePropertyChanged[T](AvaloniaProperty`1 property, Optional`1 oldValue, BindingValue`1 newValue, BindingPriority priority, Boolean isEffectiveValue)
   at Avalonia.PropertyStore.EffectiveValue`1.SetAndRaiseCore(ValueStore owner, StyledProperty`1 property, T value, BindingPriority priority, Boolean isOverriddenCurrentValue, Boolean isCoercedDefaultValue)
   at Avalonia.PropertyStore.ValueStore.SetLocalValue[T](StyledProperty`1 property, T value)
   at Avalonia.PropertyStore.ValueStore.SetValue[T](StyledProperty`1 property, T value, BindingPriority priority)
   at Avalonia.Controls.Presenters.ContentPresenter.set_Content(Object value)
   at FluentAvalonia.UI.Controls.TabViewItem.OnHeaderChanged()
   at FluentAvalonia.UI.Controls.TabViewItem.OnApplyTemplate(TemplateAppliedEventArgs e)
   at Avalonia.Controls.Primitives.TemplatedControl.ApplyTemplate()
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at FluentAvalonia.UI.Controls.TabViewStackPanel.MeasureOverride(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Layout.LayoutManager.Measure(Layoutable control)
   at Avalonia.Layout.LayoutManager.ExecuteMeasurePass()
   at Avalonia.Layout.LayoutManager.InnerLayoutPass()
   at Avalonia.Layout.LayoutManager.ExecuteLayoutPass()
   at Avalonia.Layout.LayoutManager.ExecuteQueuedLayoutPass()
   at Avalonia.Media.MediaContext.FireInvokeOnRenderCallbacks()
   at Avalonia.Media.MediaContext.RenderCore()
   at Avalonia.Media.MediaContext.Render()
   at Avalonia.Threading.DispatcherOperation.InvokeCore()
   at Avalonia.Threading.DispatcherOperation.Execute()
   at Avalonia.Threading.Dispatcher.ExecuteJob(DispatcherOperation job)
   at Avalonia.Threading.Dispatcher.ExecuteJobsCore(Boolean fromExplicitBackgroundProcessingCallback)
   at Avalonia.Threading.Dispatcher.Signaled()
   at Avalonia.Win32.Win32Platform.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)

I'm running Avalonia UI 11.0.6 and FluentAvaloniaUI 2.0.4 on Windows 11.

Any guidance would be appreciated.

amwx commented 9 months ago

Don't store control instances in your ViewModels. Use the HeaderTemplate and ContentTemplate to control the view from a ViewModel:

 <ui:TabView IsAddTabButtonVisible="False"
             TabCloseRequested="TabView_TabCloseRequested"
             CanDragTabs="True" CanReorderTabs="True"
             TabItems="{Binding TabView}"
             SelectedItem="{Binding SelectedTab}">
     <ui:TabView.TabItemTemplate>
         <DataTemplate DataType="local:TabItem">
             <ui:TabViewItem IconSource="{Binding IconSource}"
                             Header="{Binding }"
                             Content="{Binding }">
                 <ui:TabViewItem.HeaderTemplate>
                     <DataTemplate DataType="local:TabItem">
                         <controls:MyCustomHeader />
                     </DataTemplate>
                 </ui:TabViewItem.HeaderTemplate>

                 <ui:TabViewItem.ContentTemplate>
                     <DataTemplate DataType="local:TabItem">
                         <controls:MyCustomContent />
                     </DataTemplate>
                 </ui:TabViewItem.ContentTemplate>
             </ui:TabViewItem>
         </DataTemplate>
     </ui:TabView.TabItemTemplate>
 </ui:TabView>

If you need more control over specific control views for each tab, then you need to wrap your headers and content ViewModels in their own classes and create DataTemplates for each. Read up and familiarize yourself with how MVVM works:

public class TabItemViewModel
{
    public TabItemHeaderBase Header { get; }
    public TabItemContentBase Content { get; }
}

// Base classes for TabItem headers and contents - derived classes can add extra functionality
public class TabItemHeaderBase
{
}

public class TabItemContentBase
{
}

Create DataTemplates to support each of these:

<Window.DataTemplates>
    <DataTemplate DataType="vm:TabItemHeader1">
         ....
    </DataTemplate>

    ...
</Window.DataTemplates>

Then just adjust your TabView like:

<ui:TabView IsAddTabButtonVisible="False"
            TabCloseRequested="TabView_TabCloseRequested"
            CanDragTabs="True" CanReorderTabs="True"
            TabItems="{Binding TabView}"
            SelectedItem="{Binding SelectedTab}">
    <ui:TabView.TabItemTemplate>
        <DataTemplate DataType="local:TabItem">
            <ui:TabViewItem IconSource="{Binding IconSource}"
                            Header="{Binding Header}"
                            Content="{Binding Content}">
            </ui:TabViewItem>
        </DataTemplate>
    </ui:TabView.TabItemTemplate>
</ui:TabView>
rasta-mouse commented 9 months ago

Ahhh, yes I understand. I see now that my question was pretty stupid 😥 Thanks for taking the time to provide such a detailed response.