Add multiple views inside a view using WPF and Caliburn.Micro

30,093

Solution 1

EDIT - New (more complete) Answer Below:

Ok, C.M is doing a lot of stuff for you, it's all about getting your classes and xaml prepared for C.M to be able to find it. As said above, I prefer to be write code explicit, rather than rely in implicit code assumptions by the framework.

So, the Bootstrapper, from the default C.M project is just fine.

public class AppBootstrapper : Bootstrapper<MainViewModel>
{
    // ... You shouldn't need to change much, if anything
}

The section `Bootstrapper' is very important, is it indicates which ViewModel is your first, or main screen, when the app starts up.

[Export(Typeof(MainViewModel))]
public class MainViewModel : Screen,  IShell
{
    [ImportingConstructor]
    public MainViewModel(YourFirstViewModel firstViewModel, YourSecondViewModel secondviewModel) // etc, for each child ViewModel
    {
    }
}

In the [ImportingConstructor] you don't need to do anything other than specify that the MainViewModel requires the presence of the other ViewModels. In my particular case, I like my MainViewModel to be a container, and container only, the event logic is handled elsewhere. But you could just as easily have your Handle logic here - but that's a while other discussion.

Now each child View Model also needs to export themselves so C.M knows where to find them.

[Export(Typeof(YourFirstViewModel))]
public class YourFirstViewModel : IShell
{
    // VM properties and events here
}

No need to specify an Importing Constructor if you are just using a default constructor.

Now, each of your Views for these will look something like:

<UserControl x:Class="Your.Namespace.MainView"
             xmlns:views="clr-namespace:Your.Namespace.Views"
             xmlns:cal="http://www.caliburnproject.org"
             cal:Bind.Model="Your.Namespace.ViewModels.MainViewModel"
             MinWidth="800" MinHeight="600">
    <StackPanel x:Name="RootVisual">
        <views:YourFirstView />
        <views:YourSecondView />
        <!-- other controls as needed -->
    </StackPanel>
</UserControl>

XAMl or one of the child-views

<UserControl x:Class="Your.Namespace.Views.YourFirstView"
             xmlns:cal="http://www.caliburnproject.org"
             cal:Bind.Model="Your.Namespace.ViewModels.YourFirstViewModel"
             MinWidth="800" MinHeight="600">
    <Grid x:Name="RootVisual">
        <!-- A bunch of controls here -->
    </Grid>
</UserControl>

What the heck is actually going on here?

Well, C.M sees in the bootstrapper, that MainViewModel is the starting point because of the line specifying public class AppBootstrapper : Bootstrapper<MainViewModel>. MainViewModel requires that a YourFirstViewModel and YourSecondViewModel (and other ViewModels) are required in it's constructor, so C.M constructs each one. All of these ViewModels end up in the IoC (making your life much easier later - again, a whole other discussion).

C.M handles assigning the datacontext, on your behalf, to each of the views because you specify which VM to bind to with the line like cal:Bind.Model="Your.Namespace.ViewModels.YourFirstViewModel"

With any luck, that should get you started. Also refer to the C.M example project Caliburn.Micro.HelloEventAggregator as it does exactly what you are looking for (Although, it's described as an Event Aggregator demo, which is also very useful - but again, another discussion)

(Original Answer for reverence, below)

You need to do this:

<UserControl x:Class="Your.Namespace.Here.YourView"
             xmlns:cal="http://www.caliburnproject.org"
             cal:Bind.Model="Your.Namespace.Here.YourViewModel"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="1024">
  <YourControlLayout />
</UserControl>

Notice the line cal:Bind.Model="Your.Namespace.Here.YourViewModel" which specifies the exact View Model to bind this View to.

Don't forget to export your class type, or c.m can't find it.

[Export(typeof(YourViewModel))]
public class YourViewModel : IShell
{
    ...
}

Then you can nest your User Controls as you see fit. It's a very good way to make use of C.M, and you will find it highly scalable. The only weakness is that the View and ViewModel must be in the same project (as far as I can tell). But the strength of this approach is you can separate the View and View Model classes into different Namespaces (within the same project) if you wish, to keep things organized.

As a commentary on c.m I prefer this method, actually, even if I don't have to nest View UserControls and such. I would rather explicitly declare witch VM a View is bound to (and still let C.M handle all the heavy lifting in IoC) than let c.m "figure it out" from implied code.

Even with a good framework: explicit code is more maintainable than implied code. Specifying the bound View Model has the benefit of clearly stating what your data context is expected to be, so you won't need to guess later.

Solution 2

A better approach is to use ContentControl on your main view, and give it the same name as a public property on your MainViewModel which is of type MyControlViewModel. E.g.

MainView.xaml

<ContentControl x:Name="MyControlViewModel" />

MainViewModel.cs

// Constructor
public MainViewModel()
{
  // It would be better to use dependency injection here
  this.MyControlViewModel = new MyControlViewModel();     
}

public MyControlViewModel MyControlViewModel
{
  get { return this.myControlViewModel; }
  set { this.myControlViewModel = value; this.NotifyOfPropertyChanged(...); }
}

Solution 3

in file App.xaml.cs, in method GetInstance add the following lines

protected override object GetInstance(Type service, string key)
{
    if (service == null && !string.IsNullOrWhiteSpace(key))
    {
        service = Type.GetType(key);
        key = null;
    }
    // the rest of method
}
Share:
30,093
diamondfish
Author by

diamondfish

Updated on April 05, 2020

Comments

  • diamondfish
    diamondfish about 4 years

    I'm trying to learn using Caliburn.Micro with WPF. How can I add multiple views inside a view?

    <Window x:Class="ProjectName.Views.MainView"
             ...>
    <Grid>
            <views:MyControlView  />
    </Grid>
    </Window>
    

    Another view, with viewmodel: MyControlViewModel

    <UserControl x:Class="ProjectName.Views.MyControlView"
             ...>
    <Grid>
        ...
    </Grid>
    </UserControl>
    

    If i just add the view, it won't detect that it has a viewmodel with the appropriate name. How can i bind this to it?

    I have tried out with different bootstrappers and using something like cal:Bind.Model="path/classname/merge of the two". Have tried to add that to the mainview and to the usercontrol (MyControlView). I'm VERY grateful for any help regarding this matter. I'm pretty much stuck, and I really want to use Caliburn.Micro :)

    Best Regards, diamondfish

    Edit: I still can't get it to work, the problem seems to be in the bootstrapper or something else. But just to clarify, here is my code I'm running for a testproject.

    MainView xaml:

    <Window x:Class="Test.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
        xmlns:views="clr-namespace:Test.Views"
        Title="MainWindow" Height="360" Width="640">
    <Grid>
        <views:MyControlView />
    </Grid>
    

    MainViewModel code:

    public partial class MainViewModel : PropertyChangedBase
    {
    }
    

    MyControlView xaml:

    <UserControl x:Class="Test.Views.MyControlView"
             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:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
             cal:Bind.Model="Test.MyControlViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="{Binding MyProp}"/>
    </Grid>
    

    MyControlView code:

    public class MyControlViewModel : PropertyChangedBase
    {
        public string MyProp
        {
            get { return "Working"; }
        }
    }
    

    Screenshot of the error: http://clip2net.com/s/1gtgt

    I have tried

    cal:Bind.Model="Test.ViewModels.MyControlViewModel" 
    

    as well. Also tried the cal-reference:

    xmlns:cal="http://www.caliburnproject.org"
    

    Screenshot of my project http://clip2net.com/s/1gthM

    Since the documentation mostly is for silverlight and sometimes is for Caliburn and not CM, I might have implemented the bootstrapper wrong. For this test-project, it's just like this: (with the .xaml-change in App.xaml)

    public class BootStrapper : Bootstrapper<MainViewModel>
    {
    }
    

    Please help me out here! It seems like it is some basic stuff I'm missing :)

    • EtherDragon
      EtherDragon over 12 years
      -edited post to include MVVM tag, welcome to S.O!
    • EtherDragon
      EtherDragon over 12 years
      Check the anser - I added a section about exporting the type. This is an important requirement for c.m to find the ViewModel related to the View.
  • diamondfish
    diamondfish over 12 years
    Thank you for stating out how it should be. Alhough, I can't get it to work. Please look over my original post and check the updated part. Would love to get this fixed :)
  • EtherDragon
    EtherDragon over 12 years
    You're on the right track - we're just missing something silly. Added info on Export.
  • diamondfish
    diamondfish over 12 years
    Gah, it just won't work :S I tried adding a MefBootstrapper class and implemented the IShell interface, but still nothing. If you have the time to check my project, please feel free to do so :) johanbjarnle.se/temp/CaliburnTest.rar
  • diamondfish
    diamondfish over 12 years
    If you, or someone else, have the time to look at it and then upload an edited version somewhere, that would be awesome :)
  • diamondfish
    diamondfish over 12 years
    I got this to work. But it doesn't seem to utilize C.M as much? Although a good way of doing it, thanks a lot!
  • diamondfish
    diamondfish over 12 years
    I also found out that I can't see the interface of MyControl in the MainView while editing in VS when using the ContentControl. Is there a way to do that?
  • EtherDragon
    EtherDragon over 12 years
    caliburnmicro.codeplex.com/… This example shows how to do exactly what you are after. A Main View Model (or Shell) with two sub-view models inside.
  • devdigital
    devdigital over 12 years
    You are using CM, that is what is matching the name of the ContentControl to the name of your view model property, locating the view, injecting the view into the ContentControl, and binding that view's controls to the view model properties. This would be the recommended approach to view composition with Caliburn.Micro.
  • devdigital
    devdigital over 12 years
    As for the conventions being ran inside the visual studio designer, I don't believe this is possible at the moment, so you would need to edit the interfaces of each view separately.
  • Mounhim
    Mounhim over 12 years
    I used this answer for my solution. I got the views to load, but somehow the controls in the child view are not boubd to proprties in the model class. I am using the NotifyPropertyChanged as base class for the child viewmodel as well as the iShell interface. Also using the MEFBootstrapper from the CM example. Without it didnt work. UPDATE: Had to use Binding Source=propname instead of Binding = propname
  • Mounhim
    Mounhim over 12 years
    UPDATE: Strange: The control on my usercontrol (child) gets bound to the property on the mainviewmodel, but not on the childviewmodel. I really am lost here. So MainViewModel.Prop1 does show its value in Textblock on ChildView1, but ChildViewModel.Prop1 does not show its value in Textblock on ChildView1.
  • Dave
    Dave about 10 years
    Fixed my value cannot be null error on using Ninject in my bootstrapper. Thanks a lot :)
  • Adam Plocher
    Adam Plocher over 9 years
    Great answer, this should replace Caliburn's Getting Started / n00b guide :)