WPF Caliburn.Micro and TabControl - changing tab, not changing model
I'm kind of new to WPF and MVVM and Caliburn, so I ask you to indulge :)
I have a problem with binding TabControl with dynamically generated models. The tabcontrol is created correctly, but changing the tab does not switch the viewmodel used to bind the "view" (I am using the first viewmodel approach)
I made my solution based on this question: WPF Caliburn.Micro and TabControl with UserControls problem
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 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 a TabControl with many tabs. Each tab has the same "view" (declared in the DataTemplate), but I want to use different viewModels to bind this view (to be specific - the same model class [ViewTabModel], but with different data)
The size of the tabs must be declared at runtime, as well as the data that must be in the ViewTabModel.
In the example below - I have two tabs, but changing them does not change the shortcut (I have all the timestamps: "Foo1" even if I click on the "Foo2" tab)
I am using caliburn.micro as a frame - with autoloading (if it matters) AND I am using the Changed.Fody property ( https://github.com/Fody/PropertyChanged ) to omit all properties changed in the viewmodels.
What am I doing wrong?
=== UPDATE ===
Attaching a minimal solution to reproduce:
https://wetransfer.com/downloads/0b909bfd31a588dda99655f366eddad420170801192103/1d094a
Plesi, help! :)
=== UPDATE 2 ===
Something incomprehensible in my question? :) Still no comments, no anwsers-events with bounty on it.
=== UPDATE 3 ===
I already posted the COMPLETE view page (xaml) and the COMPLETE model code (that's just this)
I am posting AppBoostraper.cs and AppWindowManager.cs as well (but I suppose this is not the case for him)
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();
}
}
}
Inherits 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 === Apparently changing control with:
cal: Bind.Model = "{Binding}" x: Name = "DisplayName"
in
Content = "{Binding DisplayName}"
Did the job. Although I'm not entirely sure why?
Now I want to do the same. Only this time, I want my view to be tied. So the 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:
<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 ending === I've been hacked. I have to adhere to the first ViewModel convention (as I declared which I am using) and my attempts were first watched. So I changed it to:
<ContentControl cal:View.Model="{Binding ActiveItem}" />
But nothing is displayed then
If I declare it like this:
<ContentControl cal:View.Model="{Binding}" />
There is only a message that says "Unable to find a view for: [my_namspece] .ViewTabModel It was weird and made me think. Maybe I am not following the convention. And it was true ...
My model was called:
ViewTabModel
Whereas it should be:
ViewTabViewModel
exactly the same with the presentation. It should be called:
ViewTabView.xaml
After that, such a construction:
<ContentControl cal:View.Model="{Binding}" />
Works correctly! Thanks to arcticwhite and grek40 for leading me to this solution.
source to share
Now I have had time to test your sample project ... as I commented you must select the correct binding type ...
From All About Actions , I think there will be other sources of documentation listing the same basic information:
- Bind.Model - View-First - Sets the Action.Target and DataContext properties for the specified instance. Applies a legend to the view. String values are used to resolve an instance from IoC. (Use for root nodes like Window / UserControl / Page.)
- Bind.ModelWithoutContext - View-First - Sets the Action.Target to the specified instance. Applies a legend to the view. (Usage inside DataTemplate.)
- View.Model - ViewModel-First - Finds a view for the specified VM instance and injects it into the content site. Sets the virtual machine to Action.Target and DataContext. Applies a legend to the view.
As I said, I'm not an expert on caliber, so I had to try ... the second option looked best to me ("Use inside DataTemplate"), so here's the output:
FROM
<Label cal:Bind.Model="{Binding}" x:Name="DisplayName" Height="200" Width="200" />
replaced by
<Label cal:Bind.ModelWithoutContext="{Binding}" x:Name="DisplayName" Height="200" Width="200" />
and his work.
Actually, I would advise instead to inject the model on the surrounding stack (datatemplate root)
<StackPanel cal:Bind.ModelWithoutContext="{Binding}">
<Label x:Name="DisplayName" Height="200" Width="200" />
</StackPanel>
source to share
Ok ... I have already worked with Caliburn.Micro
, so I can say that I have some experience and not a professional, but I manage to get it to work.
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 file
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>
PS Why did I remove your displayName variable in the constructor ... Because you don't need it, it is already in Caliburn:Micro.Screen
as a property.
Edit # 2 The
convention will work, just add cal:Bind.ModelWithoutContext="{Binding}"
to yours Label
(edited answer).
source to share