2

So I've just started out using Catel this past week but I'm having trouble getting a tabbed interfaced working. I've been using the following resource Using a tabbed interface with MVVM (https://catelproject.atlassian.net/wiki/display/CTL/Using+a+tabbed+interface+with+MVVM#UsingatabbedinterfacewithMVVM-CreatingClosableTabItem):

I have a MainWindow (catel:Window) which contains a TabControl with xmlns:controls="clr-namespace:AutoProgram.UI.Controls":

<Border Background="#50FFFFFF" BorderBrush="{StaticResource WindowFrameBrush}" BorderThickness="5" Margin="-6" Padding="0">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Border Grid.Row="0" Grid.Column="0" Background="{StaticResource WindowFrameBrush}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" CornerRadius="0,0,0,0" Margin="0" Padding="0">
            <Grid>
                <TextBlock Foreground="White" FontWeight="Bold" VerticalAlignment="Center" Margin="10,2,10,2" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=Title}"/>
                <Button Content="X" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="1" FontSize="7" Width="15" Height="15" Padding="0" Command="ApplicationCommands.Close"/>
            </Grid>
        </Border>
        <catel:StackGrid Grid.Row="1" Grid.Column="0">
            <catel:StackGrid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </catel:StackGrid.RowDefinitions>
            <catel:StackGrid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
            </catel:StackGrid.ColumnDefinitions>

            <ItemsControl x:Name="ItemsControlAutomators" Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Automators}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <UniformGrid Columns="3" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Button x:Name="Button" Content="{Binding Automator.Name, Mode=OneWay}" Command="{Binding ElementName=ItemsControlAutomators, Path=DataContext.RunAutomator}" CommandParameter="{Binding}"></Button>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </catel:StackGrid>

        <catel:TabControl x:Name="TabControlAutomators" Grid.Row="2" Grid.Column="0" Margin="-2" LoadTabItems="LazyLoading">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <controls:ClosableTabItem Title="{Binding ViewModel.Title}" CanClose="{Binding CanClose}" />
                </DataTemplate>
            </TabControl.ItemTemplate>

            <TabControl.ContentTemplate>
                <DataTemplate>
                    <ContentControl Content="{Binding ViewModel, Converter={catel:ViewModelToViewConverter}}" />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </catel:TabControl>
    </Grid>
</Border>

Relevant MainWindow.xaml.cs

public partial class MainWindow
{
    public MainWindow()
    {
        InitializeComponent();

        var serviceLocator = this.GetServiceLocator();
        var tabService = serviceLocator.ResolveType<ITabService>() as TabService;

        tabService?.SetTabControl(TabControlAutomators);
    }
}

Relevant MainWindowViewModel.cs and location of failure:

public class MainWindowViewModel : ViewModelBase
{
    private readonly IAutomatorService _automatorService;
    private readonly ITabService _tabService;

    public MainWindowViewModel(IAutomatorService automatorService, ITabService tabService)
    {
        Argument.IsNotNull(() => automatorService);
        Argument.IsNotNull(() => tabService);

        _automatorService = automatorService;
        _tabService = tabService;

        RunAutomator = new Command<AutomatorModel>(OnRunAutomator, OnRunAutomatorCanExecute);
    }

    public override string Title => "AutoProgram";

    public ObservableCollection<AutomatorModel> Automators
    {
        get { return GetValue<ObservableCollection<AutomatorModel>>(AutomatorsProperty); }
        set { SetValue(AutomatorsProperty, value); }
    }

    public static readonly PropertyData AutomatorsProperty = RegisterProperty("Automators", typeof(ObservableCollection<AutomatorModel>), () => new ObservableCollection<AutomatorModel>());

    public Command<AutomatorModel> RunAutomator { get; private set; }

    public async void OnRunAutomator(AutomatorModel automatorModel)
    {
        Debug.WriteLine($"NAME: {automatorModel.Automator.Name}");
        _tabService.AddAndActivate<AutomatorViewModel>(new AutomatorViewModel(automatorModel), true);   // Throws a null exception in TabItem.cs
        //_tabService.AddAndActivate<AutomatorViewModel>(new AutomatorViewModel(), true);   // But this works (sort of, see bottom error).
    }

}

TabServiceExtensions.cs

public static class TabServiceExtensions
{
    public static TabItem Add<TViewModel>(this ITabService tabService, object dataContext = null, bool canClose = false)
        where TViewModel : IViewModel
    {
        Argument.IsNotNull(() => tabService);

        var tabItem = CreateTabItem<TViewModel>(tabService, dataContext);
        tabItem.CanClose = canClose;

        tabService.Add(tabItem);

        return tabItem;
    }

    public static TabItem AddAndActivate<TViewModel>(this ITabService tabService, object dataContext = null, bool canClose = false)
        where TViewModel : IViewModel
    {
        Argument.IsNotNull(() => tabService);

        var tabItem = Add<TViewModel>(tabService, dataContext, canClose);
        tabService.Activate(tabItem);

        return tabItem;
    }

    public static TabItem CreateTabItem<TViewModel>(this ITabService tabService, object dataContext)
        where TViewModel : IViewModel
    {
        Argument.IsNotNull(() => tabService);

        var dependencyResolver = tabService.GetDependencyResolver();
        var viewModelFactory = dependencyResolver.Resolve<IViewModelFactory>();
        var vm = viewModelFactory.CreateViewModel<TViewModel>(typeof(TViewModel), dataContext);

        return new TabItem(vm);
    }

    public static void AddAndActivate(this ITabService tabService, TabItem tabItem)
    {
        Argument.IsNotNull(() => tabService);
        Argument.IsNotNull(() => tabItem);

        tabService.Add(tabItem);
        tabService.Activate(tabItem);
    }
}

TabItem.cs

public class TabItem
{
    public TabItem(IViewModel viewModel)
    {
        Argument.IsNotNull(() => viewModel);

        ViewModel = viewModel;
        CanClose = true;

        if (!viewModel.IsClosed)
        {
            viewModel.ClosedAsync += OnViewModelClosed;
        }
    }

    public IViewModel ViewModel { get; private set; }

    public bool CanClose { get; set; }

    public object Tag { get; set; }

    public event EventHandler<EventArgs> Closed;

    private async Task OnViewModelClosed(object sender, ViewModelClosedEventArgs e)
    {
        var vm = ViewModel;
        if (vm != null)
        {
            vm.ClosedAsync -= OnViewModelClosed;
        }

        Closed.SafeInvoke(this);
    }
}

I want the TabItems to be AutomatorViewModels. The latter is initialised as such:

    public AutomatorViewModel(AutomatorModel automatorModel)
    {
        Title = "Test";
    }

But the above code throws a null Exception in TabItem.cs. If I omit the constructor argument, i.e., change it to public AutomatorViewModel() the tab(s) do get created with "Test" titles. Although in that case get the following error when manually closing these tab(s): System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.TabControl', AncestorLevel='1''. BindingExpression:Path=TabStripPlacement; DataItem=null; target element is 'TabItem' (Name=''); target property is 'NoTarget' (type 'Object')

App.xaml.cs

    protected override void OnStartup(StartupEventArgs e)
    {
        LogManager.AddDebugListener();

        Log.Info("Starting application");

        StyleHelper.CreateStyleForwardersForDefaultStyles();

        var serviceLocator = ServiceLocator.Default;
        serviceLocator.RegisterType<IAutomatorService, AutomatorService>();
        serviceLocator.RegisterType<ITabService, TabService>();

        // SOME THINGS I'VE TRIED/CURRENTLY TRYING.
        //var dependencyResolver = this.GetDependencyResolver();
        //var viewModelLocator = dependencyResolver.Resolve<IViewModelLocator>();
        //viewModelLocator.Register<AutomatorView, AutomatorViewModel>();
        //viewModelLocator.Register(typeof(AutomatorView), typeof(AutomatorViewModel));
        //viewModelLocator.Register<MainWindow, MainWindowViewModel>();

        Log.Info("Calling base.OnStartup");
        base.OnStartup(e);
    }

Catel debugging info:

11:22:39:117 => [DEBUG] [Catel.MVVM.ViewModelBase] [8] Creating view model of type 'AutomatorViewModel' with unique identifier 2

11:22:39:117 => [DEBUG] [Catel.MVVM.ViewModelCommandManager] [8] Creating a ViewModelCommandManager for view model 'AutoProgram.UI.ViewModels.AutomatorViewModel' with unique identifier '2'

11:22:39:118 => [DEBUG] [Catel.MVVM.ViewModelCommandManager] [8] Created a ViewModelCommandManager for view model 'AutoProgram.UI.ViewModels.AutomatorViewModel' with unique identifier '2'

11:22:39:119 => [DEBUG] [Catel.MVVM.ManagedViewModel] [8] Added view model instance, currently containing '1' instances of type 'AutoProgram.UI.ViewModels.AutomatorViewModel'

11:22:39:123 => [DEBUG] [Catel.IoC.TypeFactory] [8] Creating instance of type 'AutoProgram.UI.ViewModels.AutomatorViewModel' using specific parameters. No constructor found in the cache, so searching for the right one

11:22:39:124 => [DEBUG] [Catel.IoC.TypeFactory] [8] Checking if constructor 'public ctor(AutomatorModel automatorModel)' can be used

11:22:39:126 => [DEBUG] [Catel.IoC.TypeFactory] [8] Constructor is not valid because value 'AutoProgram.UI.ViewModels.AutomatorViewModel' cannot be used for parameter 'AutoProgram.UI.ViewModels.AutomatorViewModel'

11:22:39:126 => [DEBUG] [Catel.IoC.TypeFactory] [8] The constructor is valid and can be used

11:22:39:127 => [DEBUG] [Catel.IoC.TypeFactory] [8] No constructor could be used, cannot construct type 'AutoProgram.UI.ViewModels.AutomatorViewModel' with the specified parameters

11:22:39:128 => [DEBUG] [Catel.IoC.TypeFactory] [8] Creating instance of type 'AutoProgram.UI.ViewModels.AutomatorViewModel' using specific parameters. No constructor found in the cache, so searching for the right one

11:22:39:128 => [DEBUG] [Catel.IoC.TypeFactory] [8] Checking if constructor 'public ctor(AutomatorModel automatorModel)' can be used

11:22:39:129 => [DEBUG] [Catel.IoC.TypeFactory] [8] Constructor is not valid because parameter 'automatorModel' cannot be resolved from the dependency resolver

11:22:39:129 => [DEBUG] [Catel.IoC.TypeFactory] [8] The constructor is valid and can be used

11:22:39:130 => [DEBUG] [Catel.IoC.TypeFactory] [8] No constructor could be used, cannot construct type 'AutoProgram.UI.ViewModels.AutomatorViewModel' with the specified parameters

11:22:39:130 => [DEBUG] [Catel.MVVM.ViewModelFactory] [8] Could not construct view model 'AutoProgram.UI.ViewModels.AutomatorViewModel' using injection of data context 'AutomatorViewModel' AutoProgram.UI.vshost.exe Error: 0 : 11:22:39:131 => [ERROR] [Catel.Argument] [8] Argument 'viewModel' cannot be null Exception thrown: 'System.ArgumentNullException' in Catel.Core.dll


EDIT #1:

  • added debugging info (uncommented LogManager.AddDebugListener();)
  • added App.xaml.cs

EDIT #2:

Found a workaround by changing the view model's initialisation to a constructorless one, and setting the model property explicitly, as follows:

_tabService.AddAndActivate(new AutomatorViewModel { AutomatorModel = automatorModel }, false);

loupceuxl
  • 21
  • 3

1 Answers1

0

You should pass in the DataContext object, not the ViewModel into the AddAndActivate. So this should work (and will construct & inject the vm for you):

_tabService.AddAndActivate<AutomatorViewModel>(automatorModel, true);
Geert van Horrik
  • 5,689
  • 1
  • 18
  • 32
  • I've tried that and it didn't work. Same debugging errors. I don't know how and where to register a viewmodel parameter. I'm also having asynchronous issues with the nested tabcontrol. Cheers anyway. – loupceuxl Feb 04 '17 at 00:06