Question

I have a List View in which there are two Child Views. One is the Display View and another is Edit View. Here is how I have defined the List (Parent) view. Note that I want the two child UserControl's to occupy different space in the Parent.

<UserControl x:Class="RelayAnalysis_UI.Views.Relays.RelayListView"
    ....

    <ContentControl x:Name="GroupDetail" Grid.Row="2" />
    <TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0"/>
</UserControl>

Then In my view model, I activate these items based on user responses in the following manner

**View Model **

[Export(typeof(RelayListViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class RelayListViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<Group> {
    ....
    public void Edit() { //Requested Edit
        RelayEditViewModel viewModel = TryAndLocateViewModel(SelectedRelay.Group.Rack.Id, SelectedRelay.Group.Id);
        ActivateItem(viewModel);
    }
    ....

    public void ViewGroupDetail(Relay relay) { //Requested View
        GroupDetailViewModel viewModel = container.GetExportedValue<GroupDetailViewModel>();
        ActivateItem(viewModel);
    }

The above code works but the Detail View is loaded in the Tabs space (the space meant for the Edit View). Actually, the ActivateItem(viewModel) does pick up the correct type of view to display but it is loaded in the wrong place for the Display View, that is, the Display View is loaded in the Edit View's space on the screen. Surely I am missing some obvious stuff.

In summary, how do we get two UserControls defined in a Parent UserControl to activate in its own space?

Edit - 1:

Here are two Screen Shots which show where I need to load the Edit and Detail Views respectively.

Edit Screen Loaded

View Screen Loaded (Should not load in Edit Area)

As you can see in the second screenshot, the Detail View gets loaded in the Detail Area as well as the Edit Area(Tabs). I wan't the Detail View only to appear in the Detail Area. The Edit Area is only for the Edit View.

Here is the code that I have used to generate the screen shots.

The Main View that houses both views

<UserControl x:Class="RelayAnalysis_UI.Views.Relays.RelayListView"
    <Grid>
         ....
                     <ContentControl x:Name="GroupDetail" HorizontalContentAlignment="Left" 
                                        cal:View.Context="GroupDetail" cal:View.Model="{Binding ActiveItem}"/>
                    <TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0"
                                cal:View.Context="RelayEdit" cal:View.Model="{Binding ActiveItem}"/>
     </Grid>
</UserControl>

Edit 2: I think I am very close to get it working. As per your suggestions I modified the Main(Parent) container as below.

<UserControl x:Class="RelayAnalysis_UI.Views.Relays.RelayListView"
            <ContentControl x:Name="GroupDetail" HorizontalContentAlignment="Left" />
                <TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0" />

The Edit Screen and Detail Screens now appear in their proper places. However, the Detail ViewModels OnActivate is not called upon so I get a blank Detail View with no variables populated. All loading of Details View field is done on the OnActivate() override. Here is how my GroupDetailViewModel is defined

[Export(typeof(GroupDetailViewModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class GroupDetailViewModel : Screen {
    ...
    protected override void OnActivate() {
        base.OnActivate();
    ..
    } 

So certainly, I am missing some attribute. Or will I have to call some method on the GroupDetailViewModel to load the details manually ?

Was it helpful?

Solution

Removed original answer because it was long and doesn't really help out much

Edit:

Ok so disregard the above - it looks like you are trying to load two different views over two different viewmodels, which as far as I know is not what Context is designed for. The Context property loads two different views over the same viewmodel e.g. in your XAML:

<ContentControl x:Name="GroupDetail" HorizontalContentAlignment="Left" 
    cal:View.Context="GroupDetail" 
    cal:View.Model="{Binding ActiveItem}"/>
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource  TabControlStyle}" Margin="5,0,0,0" 
    cal:View.Context="RelayEdit" 
    cal:View.Model="{Binding ActiveItem}"/>

Given a VM with a name of RelayEditViewModel activated via ActivateItem() CM will try to load the following views:

RelayEdit.GroupDetail for the content control

RelayEdit.RelayEdit for the tab control

See:

http://caliburnmicro.codeplex.com/wikipage?title=View%2fViewModel%20Naming%20Conventions&referringTitle=Documentation

...

If you try to load another ViewModel, the same conventions will apply to find the view

GroupDetailViewModel results in

GroupDetail.GroupDetail for the content control

GroupDetail.RelayEdit for the tab control

It sounds like this isn't what you want (and I'm not sure why anything is loading at all - what namespace are your views in? Have you customised the view locator?)

I'm still trying to get my head round the lifecycle support you require but it sounds like you want the edit view to be lifecycle managed (since you want the load/save/guard type support) but the detail view is to be read-only and doesn't care if it's closed without being guarded

In that case you probably want to add a property to your ViewModel which will hold a reference to the details viewmodel but don't activate it ... just set the property without calling ActivateItem(vm)

example:

[Export(typeof(RelayListViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class RelayListViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<Group> {
....
// Backing field + prop to hold the details view - the content control will bind to this
private IScreen _details;
public IViewAware Details { get { } set { } } // Implement standard NotifyOfPropertyChange here for this property

public void Edit() { //Requested Edit
    RelayEditViewModel viewModel = TryAndLocateViewModel(SelectedRelay.Group.Rack.Id, SelectedRelay.Group.Id);
    ActivateItem(viewModel);
}
....

public void ViewGroupDetail(Relay relay) { //Requested View
    GroupDetailViewModel viewModel = container.GetExportedValue<GroupDetailViewModel>();
    // Instead of activating, just assign the VM to the property and make sure Details calls NotifyOfPropertyChange to let CM know to start the binding logic
    Details = viewModel;
}

Then in your XAML

<!-- Just bind the details view to the Details property -->
<ContentControl x:Name="Details" HorizontalContentAlignment="Left" /> 
<!-- Leave this as-is, as it's working ok -->
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource  TabControlStyle}" Margin="5,0,0,0" /> 

(I've assumed that you are using the TabControls default conventions above, but tweak if neccessary)

You can use the same VM for both the details and the edit view as long as you set the Context property accordingly.

Let me know if that helps

Edit:

Just to answer the question about MVVM and coupling etc...

All you are doing is composing a more complex viewmodel from several simpler viewmodels (and therefore a more complex view from several simpler views). As long as your reference to the details VM is not a concrete type, there is very loose coupling between the VMs. You could assign ANY viewmodel type that implements that interface into the Detail property on the main VM and CM will try to locate the view for it and build the interface. This is perfectly fine (you can use your IoC to get the type for the details window if needed)

If your details view needs lifecycle you should inherit from Screen, but I doubt that your details view needs activation (since it's just a details view and is ready only) so just implementing IViewAware and inheriting from PropertyChangedBase will do. The edit view, however, needs to have lifecycle and therefore should inherit from Screen.

Conductor already contains an ActiveItem property, and provides management of lifecycle for child items activated via ActivateItem(), all you are doing is creating an extra 'bolt-on' property for your conductor which references the additional vm (i.e. you need ActiveItem and Details)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top