Caliburn.Micro : How to bind a specific Item of Conductor.Collection.AllActive to a ContentControl
Asked Answered
T

3

2

My goal is to have 4 different active ViewModels displayed in a grid on the ShellView. The issue is that I have not been able to figure out how to wire up a ContentControl to a specific Item in Items of the Conductor. How can his be done?

Here is a simplified version of what I and trying to do.

SolutionExplorer

ShellViewModel:

namespace ContentControlTest.ViewModels
{
    public class ShellViewModel : Conductor<object>.Collection.AllActive
    {
        public ShellViewModel()
        {
            ActivateItem(new UC1ViewModel());
            ActivateItem(new UC2ViewModel());
            ActivateItem(new UC3ViewModel());
            ActivateItem(new UC4ViewModel());
        }
    }
}

ShellView:

<Window x:Class="ContentControlTest.Views.ShellView"
        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:ContentControlTest.Views"
        xmlns:cal="http://www.caliburnproject.org"
        mc:Ignorable="d"
        Title="ShellView" Height="450" Width="800"
        >

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ScrollViewer Grid.Row="0" Grid.Column="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <ContentControl cal:View.Model="{Binding UC1ViewModel}" cal:View.Context="{Binding Items[0]}"/>
        </ScrollViewer>
        <ScrollViewer Grid.Row="0" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <ContentControl cal:View.Model="{Binding UC2ViewModel}" cal:View.Context="{Binding Items[1]}"/>
        </ScrollViewer>
        <ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <ContentControl cal:View.Model="{Binding UC3ViewModel}" cal:View.Context="{Binding Items[2]}"/>
        </ScrollViewer>
        <ScrollViewer Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <ContentControl cal:View.Model="{Binding UC4ViewModel}" cal:View.Context="{Binding Items[3]}"/>
        </ScrollViewer>        
    </Grid>
</Window>

For simplification each UserControl ViewModel and View are Identical:

UC#ViewModel:

namespace ContentControlTest.ViewModels
{
    public class UC1ViewModel : Screen
    {
        private string id;
        public string ID
        {
            get { return id; }
            set
            {
                id = value;
                NotifyOfPropertyChange(() => ID);
            }
        }


        public UC1ViewModel()
        {
            ID = Guid.NewGuid().ToString();
        }

    }
}

UC#View:

<UserControl x:Class="ContentControlTest.Views.UC1View"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ContentControlTest.Views"
             xmlns:cal="http://www.caliburnproject.org"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             >
    <Border BorderBrush="Black"  BorderThickness="1"> 
        <StackPanel >
            <TextBlock Text="{Binding DisplayName}"/>
            <TextBlock Text="{Binding ID}"/>
        </StackPanel>
    </Border>
</UserControl>

For testing I have tried using an ItemControl and it works but doesn't give me exactly what I want.

<ItemsControl x:Name="Items">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel></StackPanel>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
Typist answered 12/2, 2020 at 21:27 Comment(0)
T
4

You need to create properties in your ShellViewModel something like UC1, UC2, UC3 etc. You then need to change your ShellView to bind to UC1 property.

            <ContentControl x:Name="UC1" />
            ...

Caliburn Micro should do the plumbing for you.

namespace ContentControlTest.ViewModels
{
    public class ShellViewModel : Conductor<object>.Collection.AllActive
    {
        // Modify to implement INotifyPropertyChanged event...
        public UC1ViewModel UC1 { get; set }

        public ShellViewModel()
        {
            UC1 = new UC1ViewModel();
            ActivateItem(UC1);
            ActivateItem(new UC2ViewModel());
            ActivateItem(new UC3ViewModel());
            ActivateItem(new UC4ViewModel());
        }
    }
}
Tearjerker answered 13/2, 2020 at 14:1 Comment(1)
Thanks for helping this noob out, I really appreciate it! Works perfectly.Typist
G
2

The Caliburn concept of Context is used to map a view model to multiple views, usually through conventions and mapping namespaces. In this case, however, each of your view models maps to exactly one view. Hence you don't need to / should not provide a context.

Second, your view model binding cannot be resolved without exposing them as public props (as @Jack suggested). Ironically, the binding you used for Context is the right one for the view model binding.

Replacing

<ContentControl cal:View.Model="{Binding UC1ViewModel}" cal:View.Context="{Binding Items[0]}"/>

With

<ContentControl cal:View.Model="{Binding Items[0]}"/>

Should do the trick.

Given the number of items is fixed it's better to follow @Jack's approach and reference the view models in a strongly typed fashion. Rather than relying on their index in the items collection. You can use either:

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

Or

<ContentControl x:Name="UC1ViewModel" />

Which are synonymous.

As you noticed the Caliburn Conductor really shines when used in combination with ItemControl. You typically don't need to have strongly typed references to the each of the Items then. That doesn't mean you can't use the conductor as you did, you still enjoy all the benefits of the managed lifecycle.

Graduation answered 13/2, 2020 at 16:44 Comment(1)
Thank you for adding the extra insight, it really helps get an understanding for the concepts.Typist
I
1

In case anyone is having an issue implementing the [perfectly fine] accepted answer, here is a more in depth answer:

  1. Your main window that contain both (or even more than two) of your User Controls must be inherited from Caliburn.Micro.Conductor<Screen>.Collection.AllActive;
  2. Your User Controls must be inherited from Caliburn.Micro.Screen;
  3. You must also keep naming conventions in mind. If you use MenuUC as the name of a ContentControl in your View, also create a property named MenuUC in your ViewModel;
  4. Initialize your UserControl as I do in Constructor;
  5. Now you can use ActivateItem(MenuUC) and DeactivateItem(MenuUC) everywhere in your code. Caliburn.Micro automatically detects which one you want to work with.

Example XAML View code:

<Window x:Class="YourProject.Views.YourView"
        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"
        mc:Ignorable="d"
        Title="YourViewTitle" Width="900" Height="480">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="4*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- Menu Side Bar -->
        <ContentControl Grid.Row="0" Grid.Column="0" x:Name="MenuUC" />

        <!-- Panel -->
        <Border Grid.Column="1" Grid.RowSpan="2" BorderThickness="1,0,0,0" BorderBrush="#FF707070" >
            <ContentControl x:Name="PanelUC" />
        </Border>
    </Grid>
</Window>

Example C# ViewModel code:

class YourViewModel : Conductor<Screen>.Collection.AllActive
{
    // Menu Side Bar
    private MenuUCViewModel _menuUC;
    public MenuUCViewModel MenuUC
    {
        get { return _menuUC; }
        set { _menuUC = value; NotifyOfPropertyChange(() => MenuUC); }
    }

    // Panel
    private Screen _panelUC;
    public Screen PanelUC
    {
        get { return _panelUC; }
        set { _panelUC = value; NotifyOfPropertyChange(() => PanelUC); }
    }

    // Constructor
    public YourViewModel()
    {
        MenuUC = new MenuUCViewModel();
        ActivateItem(MenuUC);

        PanelUC = new FirstPanelUCViewModel();
        ActivateItem(PanelUC);
    }

    // Some method that changes PanelUC (previously FirstPanelUCViewModel) to SecondPanelUCViewModel
    public void ChangePanels()
    {
        DeactivateItem(PanelUC);
        PanelUC = new SecondPanelUCViewModel();
        ActivateItem(PanelUC);
    }
}

In the above example, ChangePanels() acts as a method to load new User Control into your ContentControl.

Also read this question, it might be help you further.

Indopacific answered 30/5, 2020 at 16:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.