15
votes

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 :)

3
-edited post to include MVVM tag, welcome to S.O!EtherDragon
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.EtherDragon

3 Answers

17
votes

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.

17
votes

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(...); }
}
1
votes

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
}