Home > OS >  Use ViewLocator inside TabControl.ContentTemplate
Use ViewLocator inside TabControl.ContentTemplate

Time:09-26

In an AvaloniaUI window, I want to have a TabControl whose tabs are added and removed from an ObservableCollection<T>. The tab's "title" (the text appearing on the tab strip) should be set inside each item of the collection, which could belong to a different type.

For that I defined a type:

public abstract class TabViewModelBase : ViewModelBase
{
    public abstract string TabHeader { get; }
}

and my collection is defined like this:

public ObservableCollection<TabViewModelBase> OpenTabs { get; } = new();

In the axaml file, this is the definition of the TabControl:

<TabControl Items="{Binding OpenTabs}">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding TabHeader}"/>
        </DataTemplate>
    </TabControl.ItemTemplate>
</TabControl>

So far, this works like a charm.

The problem begins when I also want to set up a container for the view inside each tab, which should not be a part of the contained view itself. I've tried by editing the xaml above and setting a ContentTemplate like this:

<TabControl Items="{Binding OpenTabs}">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding TabHeader}"/>
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
        <DataTemplate>
            <Border Child="{Binding}"/>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

However this results in the following error:

[Binding] Error in binding to 'Avalonia.Controls.Border'.'Child': 'Could not convert 'Project.ViewModels.TestingViewModel' to 'IControl'.'

This seems to be because ViewLocator, which automatically matches a view model to a view based on its name, is not being called. I assume this is because I've defined a DataTemplate inside TabControl.ContentTemplate.

Is it possible to instruct Avalonia to use ViewLocator inside TabControl.ContentTemplate, so that a view is selected based on its name?

CodePudding user response:

<Border Child="{Binding}"/>

Border expects an actual control as a child, not a view model. You need to use ContentControl instead. It can also have it's own data template or view locator.

CodePudding user response:

I found a way to work around the issue, by defining an IValueConverter that uses ViewLocator internally:

public class ViewModelValueConverter : IValueConverter
{
    public object? Convert(
        object value, Type targetType, object parameter, CultureInfo culture
    )
    {
        if (value == null)
            return null;

        if (
            value is ViewModelBase viewModel
            && targetType.IsAssignableFrom(typeof(IControl))
        )
        {
            ViewLocator viewLocator = new();
            return viewLocator.Build(value);
        }

        throw new NotSupportedException();
    }

    public object ConvertBack(
        object value, Type targetType, object parameter, CultureInfo culture
    )
    {
        throw new NotSupportedException();
    }
}

and using it in XAML:

<Window.Resources>
    <local:ViewModelValueConverter x:Key="variableView"/>
</Window.Resources>
<TabControl Items="{Binding OpenTabs}">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding TabHeader}"/>
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
        <DataTemplate>
            <Border Child="{Binding, Converter={StaticResource variableView}}"/>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

but it feels like there might be a simpler solution.

  • Related