3
votes

I'm kind of newbie in WPF&MVVM&Caliburn so I beg your indulgence :)

I have a problem with binding TabControl with dynamically created models. Tabcontrol is being created correctly but changing tab is not switching viewmodel used to bind "view" (I'm using viewmodel first approach)

I've made my solution basing on this question: WPF Caliburn.Micro and TabControl with UserControls issue

This is my model definition:

public interface IMainScreenTabItem : IScreen
{
}

public class MainViewTestTabsViewModel : Conductor<IMainScreenTabItem>.Collection.OneActive
{
    public MainViewTestTabsViewModel(IEnumerable<IMainScreenTabItem> tabs)
    {
        Items.Add(new ViewTabModel("Foo1"));
        Items.Add(new ViewTabModel("Foo2"));
        Items.AddRange(tabs);
    }
}

public sealed class ViewTabModel : Screen, IMainScreenTabItem
{
    public ViewTabModel(string displayName)
    {
        DisplayName = displayName;
    }
}

And here is the view MainViewTestTabsView:

<UserControl  x:Class="TestWpfApp.Views.MainViewTestTabsView"
    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TestWpfApp.Views"
    xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
    xmlns:viewModels="clr-namespace:TestWpfApp.ViewModels"
    xmlns:cal="http://www.caliburnproject.org"
    mc:Ignorable="d" Width="500" Height="500">
<Grid>
    <TabControl Name="Items">
        <TabControl.ContentTemplate>
            <DataTemplate>
                <StackPanel>
                    <Label  cal:Bind.Model="{Binding}" x:Name="DisplayName" Height="200" Width="200" />
                </StackPanel>
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
</Grid>

What I want to achieve - is to have TabControl with many tabs. Each tab has the same "view" (declared in DataTemplate) but to bind this view I want to use different viewModels (to be specific - the same model class [ViewTabModel ] but with different data)

The size of tabs is to be declared at runtime as well as the data, which should be in the ViewTabModel model.

In example below - I have two tabs, but changing them is not changing the Label (i have all the Time: "Foo1" Label, even if I click "Foo2" tab)

I use caliburn.micro as a framework - with autofac bootstrap (if it matters) And I use propertyChanged.Fody (https://github.com/Fody/PropertyChanged) to omit all the propertychanged stuff in viewmodels.

What am I doing wrong?

=== UPDATE ===

Attaching minimal reproduction solution:

https://wetransfer.com/downloads/0b909bfd31a588dda99655f366eddad420170801192103/1d094a

Plese, help! :)

=== UPDATE 2 ===

Is anything unclear about my question ?:) Still no comments, no anwsers event with bounty on it.

=== UPDATE 3 ===

I've already posted COMPLETE view page (xaml) and COMPLETE model code (this is only this)

I'm posting also AppBoostraper.cs and AppWindowManager.cs (but i suppose it is irrelevat here)

AppBoostrapper.cs

using Autofac;
using TestWpfApp.ViewModels;

namespace TestWpfApp {
    using System;
    using System.Collections.Generic;
    using Caliburn.Micro;

    public class AppBootstrapper : CaliburnMetroAutofacBootstrapper<MainViewTestTabsViewModel>
    {
        protected override void ConfigureContainer(ContainerBuilder builder)
        {
            builder.RegisterType<AppWindowManager>().As<IWindowManager>().SingleInstance();
            var assembly = typeof(ShellViewModel).Assembly;
            builder.RegisterAssemblyTypes(assembly)
                .Where(item => item.Name.EndsWith("ViewModel") && item.IsAbstract == false)
                .AsSelf()
                .SingleInstance();
        }
    }
}

It is inheriting CaliburnMetroAutofacContainer (https://github.com/ziyasal/Caliburn.Metro)

AppWindowsManager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Caliburn.Metro.Core;
using MahApps.Metro.Controls;

namespace TestWpfApp
{
    public class AppWindowManager : MetroWindowManager
    {
        public override MetroWindow CreateCustomWindow(object view, bool windowIsView)
        {
            if (windowIsView)
            {
                return view as ShellView;
            }

            return new ShellView
            {
                Content = view
            };
        }
    }
}

=== UPDATE 4 === Apprently, changing control from:

cal:Bind.Model="{Binding}" x:Name="DisplayName"

to:

Content="{Binding DisplayName}"

Did the work. Although i'm not quite sure why?

Now i want to do exactly the same. Only this time i want my view to be binded. So ViewModel is exactly the same. But this time:

MainViewTestTabsView

<UserControl  x:Class="TestWpfApp.Views.MainViewTestTabsView"
    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TestWpfApp.Views"
    xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
    xmlns:viewModels="clr-namespace:TestWpfApp.ViewModels"
    xmlns:cal="http://www.caliburnproject.org"
    mc:Ignorable="d" Width="500" Height="500">
<Grid>
    <TabControl Name="Items">
        <TabControl.ContentTemplate>
            <DataTemplate>
                <StackPanel>
                    <local:ViewTab cal:Bind.Model="{Binding}" />
                </StackPanel>
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
</Grid>

and ViewTab view is :

<UserControl  x:Class="TestWpfApp.Views.ViewTab"
    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TestWpfApp.Views"
    xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
    xmlns:viewModels="clr-namespace:TestWpfApp.ViewModels"
    xmlns:cal="http://www.caliburnproject.org"
    mc:Ignorable="d" Width="300" Height="300">
<Grid>
    <StackPanel>
        <Label x:Name="DisplayName"></Label>
    </StackPanel>
</Grid>

=== Update 5 => Happy final === I've been sugested I should stick to the ViewModel first convention (as i declared i'm using) and my attempts was somhow view first. So i've changed it to:

<ContentControl cal:View.Model="{Binding ActiveItem}" />

But nothing is rendered then

If i declare it like this :

<ContentControl cal:View.Model="{Binding}" />

There is only message saying : "Cannot find view for : [my_namspece].ViewTabModel It was weird and get me thinking. Maybe i'm not sticking to the convention. And it was true...

My model was called:

ViewTabModel

Whereas it should be :

ViewTabViewModel

exactly the same thing with view. It should be called:

ViewTabView.xaml

After that, such construction:

<ContentControl cal:View.Model="{Binding}" />

Is working correctly!! Thank you arcticwhite and grek40 for leading me to this solution

2
Are you missing binding?Ramankingdom
I used Caliburn long back. Have you tried ActivateItem(viewmodel here)?Ramankingdom
On change of tab you need to activate the view using ActivateItemRamankingdom
Maybe you can propose, where and how to put this "ActivateItem" - it is not obvious as it sounds :) I would be grateful :)Piotr
caliburnmicro.codeplex.com/…. Here they have exampleRamankingdom

2 Answers

1
votes

Now I had some time to test your sample project... as I commented, you should chose the right binding type...

From All About Actions, I guess there will be other documentation sources around, listing the same basic information:

  • Bind.Model – View-First - Set’s the Action.Target and DataContext properties to the specified instance. Applies conventions to the view. String values are used to resolve an instance from the IoC container. (Use on root nodes like Window/UserControl/Page.)
  • Bind.ModelWithoutContext - View-First - Set’s the Action.Target to the specified instance. Applies conventions to the view. (Use inside of DataTemplate.)
  • View.Model – ViewModel-First – Locates the view for the specified VM instance and injects it at the content site. Sets the VM to the Action.Target and the DataContext. Applies conventions to the view.

As said, I'm not a caliburn expert, so I had to try it... the second option looked best to me ("Use inside of DataTemplate"), so here is the working result:

From

<Label  cal:Bind.Model="{Binding}" x:Name="DisplayName" Height="200" Width="200" />

replace to

<Label cal:Bind.ModelWithoutContext="{Binding}" x:Name="DisplayName" Height="200" Width="200" />

and its working.

Actually, I'd advise to introduce the model in the surrounding stackpanel instead (the datatemplate root)

<StackPanel cal:Bind.ModelWithoutContext="{Binding}">
    <Label x:Name="DisplayName" Height="200" Width="200" />
</StackPanel>
3
votes

Okay... I have already worked with Caliburn.Micro, so I can say I have some experience, not a pro, but I manage to make it working.

Your MainViewTestTabsViewModel.cs:

 public interface IMainScreenTabItem : IScreen
    {
    }

    public class MainViewTestTabsViewModel : Conductor<IMainScreenTabItem>.Collection.OneActive
    {
        public MainViewTestTabsViewModel(IEnumerable<IMainScreenTabItem> tabs)
        {

            Items.Add(new ViewTabModel() {DisplayName = "Test"});
            Items.Add(new ViewTabModel() { DisplayName = "Test2" });
            Items.Add(new ViewTabModel() { DisplayName = "Test3" });
            Items.AddRange(tabs);
        }
    }

    public class ViewTabModel : Screen, IMainScreenTabItem
    {
        public ViewTabModel()
        {

        }
    }

And your MainViewTestTabsView.xaml

    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TestWpfApp.ViewModels"
    xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
   xmlns:viewModels="clr-namespace:TestWpfApp.Views"
    xmlns:cal="http://www.caliburnproject.org"
    mc:Ignorable="d" Width="500" Height="500">
    <Grid>
        <TabControl x:Name="Items" >
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <StackPanel>
                        <Label cal:Bind.ModelWithoutContext="{Binding}" x:Name="DisplayName" Height="200" Width="200"/>
                    </StackPanel>
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
    </UserControl>

P.S. Why I removed your displayName variable in constructor... Because you don't need it, it's already in the Caliburn:Micro.Screen as a property.

Edit #2 The convention will work, just add cal:Bind.ModelWithoutContext="{Binding}" inside your Label (Edited the answer).