Caliburn Micro -> Composing Views from multiple Vi

2019-04-02 03:12发布

How is it possible to re-use and compose parts in CM managed windows? I have found posts regarding using two UserControls to bind to the same ViewModel, but not so much if I want to have multiple views and viewmodels all composed in the same window. (a viewmodel for each view composed into a "master view")

The first part of my question would be how to break up components for re-use? If I have two areas of a window where one is a datagrid and another is a details view with labels and text boxes should these be in separate usercontrols, customcontrols or windows? Each one would ideally be stand alone so they can be separated and used in other windows.

So I would end up with 2 viewmodels and 2 views if they were separated. Now lets say I would like to create 3 windows, one window with the first view, the second with the second view and a third with both views. How do I use CM to create the window for each and wire up each view to their viewmodel? From the examples I have seen I see for the most part a single view and viewmodel in a window.

2条回答
狗以群分
2楼-- · 2019-04-02 03:45

I'm not going to claim to be an expert in CM by any means, but I've had reasonable success with a simple "benchmark explorer" I've been writing. That uses a single "shell view" that composes two other views, each with its own ViewModel. The shell view looks like this:

<Window x:Class="NodaTime.Benchmarks.Explorer.Views.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="NodaTime Benchmarks" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>...</Grid.ColumnDefinitions>
        <ContentControl x:Name="BenchmarkPicker" Grid.Column="0"/>
        <GridSplitter ... />
        <ContentControl x:Name="ResultsGraph" Grid.Column="2"/>
    </Grid>
</Window>

then ResultsGraphView and BenchmarkPickerView are each like this:

<UserControl x:Class="NodaTime.Benchmarks.Explorer.Views.ResultsGraphView"
    ... namespaces etc ...>
    <Grid>
        <Grid.RowDefinitions>...</Grid.RowDefinitions>
        <Grid.ColumnDefinitions>...</Grid.ColumnDefinitions>
        ... controls ...
    </Grid>
</UserControl>

The ShellViewModel exposes the other two ViewModels as properties. Those are then passed to the views automatically on construction. (The bootstrapper doesn't provide any way of getting them.)

Now this doesn't quite fit your description, because I don't think you could use the two individual views individually as windows - I suspect you would end up with 5 views in total:

SubViewOne - a UserControl with the first view parts
SubViewTwo - a UserControl with the second view parts
JustViewOne - a Window containing just SubViewOne
JustViewTwo - a Window containing just SubViewTwo
BothViews - a Window containing both SubViewOne and SubViewTwo

I don't think there's a way of getting around the fact that you don't want one Window within another, and the top level window has to be... well, a Window.

Hope this helps, and let me know if you want more details of the small project where I'm doing this - it's far from production quality, particularly in terms of DI, but it may be enough to help you get going.

查看更多
放我归山
3楼-- · 2019-04-02 03:57

I think I've previously done something similar to what you're asking. I'd been playing around with one of the TabControl with the intention of hosting several different tools for a game I enjoy playing.

The main tool is an item browser similar to the usual file explorer type programs, and similar to what Jon has described above. I'll explain some of the parts which may be of interest/relevance (I've removed some of the slightly obscure naming).

The main ExplorerView tab is essentially exactly the same the one Jon describes (which is hopefully a good sign - means I'm not crazy =D)

<UserControl x:Class="ItemsBrowser.Views.ItemsTabView"
<!-- namespaces -->
>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="2*"/>
        </Grid.ColumnDefinitions>
        <ContentControl x:Name="ItemsExplorer" Grid.Column="0" Grid.Row="0" />
        <GridSplitter HorizontalAlignment="Right" VerticalAlignment="Stretch" 
                      ResizeBehavior="PreviousAndNext" Width="4" Grid.Column="1" Background="#FFAAAAAA" />
        <ContentControl x:Name="PanelView" Grid.Column="2" Grid.Row="0" />
    </Grid>   
</UserControl>

The associated ViewModel holds two other ViewModels, used for composing the main explorer view:

public class ItemsTabViewModel : Conductor<IScreen>.Collection.AllActive 
{
    public ItemsViewModel ItemsExplorer { get; set; }
    public ExplorerPanelViewModel PanelView { get; set; }

    // Ctor etc.
}

The ItemsExplorer hosts a TreeView style control, allowing users to explore various categories of Item from the game. This is used in multiple places in the application, and is composed into a few different controls.

The ExplorerPanelView is a panel on the right hand side, that changes to display a number of ViewModels, based on what type of item the user is viewing. The user also have the option to toggle a few different Views over the ViewModel displayed in the ExplorerPanelView.

The ExplorerPanelView looks like:

<UserControl x:Class="MIS_PTBrochure.Views.ExplorerPanelView"
         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:cal="http://www.caliburnproject.org">
    <Grid>
        <ContentControl cal:View.Model="{Binding Path=ActiveItem}" 
                        cal:View.Context="{Binding Path=ActiveItem.State}"
                        Content="Select a folder."/>
    </Grid>
</UserControl>

And the ExplorerPanelViewModel behind:

public class ExplorerPanelViewModel : Conductor<IScreen>.Collection.OneActive,
    IHandle<ItemSelectedEvent> // More events.
{
    public ItemViewModel ItemInfo { get; set; }
    public CategoryFolderViewModel CategoryFolderInfo { get; set; }

    public ExplorerPanelViewModel()
    {
        // My helper to access the `Caliburn.Micro` EventAggregator.
        EventAggregatorFactory.EventAggregator.Subscribe(this);

        // Other code
    }

    public void Handle(ItemSelectedEvent message)
    {
        // Other code to check active status
            ItemInfo = message.selected;
            ActivateItem(ItemInfo);
    }

    protected override void OnDeactivate(bool close)
    {
        Debug.WriteLine("Deactivated " + this.ToString() + close.ToString());
        if (close) { EventAggregatorFactory.EventAggregator.Unsubscribe(this); }
        base.OnDeactivate(close);
    }

    // Other code
}

I've tried to remove a lot of non-relevant code. Essentially I'm again hosting multiple ViewModels as properties (although you could hold a collection) and activating the relevant ViewModel when an approriate event is raised by my ItemsExplorerViewModel. I'm using the Caliburn.Micro EventAggregator to handle communication between multiple ViewModels.

In theory you could dispense with the properties, and just activate the ViewModels referenced in the events themselves.

Regarding the cal:View.Context and cal:View.Model - I'm using these all the user to toggle different available View states available (each ViewModel displayed in that panel inherits from a base ViewModel class which all have a State property).

There are a few places where I pop up different windows using some of the same Views and ViewModels. To achieve this, I make use of the Caliburn.Micro WindowManager. There isn't a great deal about it in the official documentation (you're best off searching Google and the CM discussions), it pretty does what is says on the tin.

If you have a look at the Caliburn.Micro.IWindowManager interface you'll see some handy methods that you can call from a WindowManager instance.

public interface IWindowManager
{
    bool? ShowDialog(object rootModel, object context = null, IDictionary<string, object> settings = null);
    void ShowPopup(object rootModel, object context = null, IDictionary<string, object> settings = null);
    void ShowWindow(object rootModel, object context = null, IDictionary<string, object> settings = null);
}

So to pop up a new Window with a ViewModel of your choice, I did something along these lines:

// Some basic Window settings.
dynamic settings = new ExpandoObject();
    settings.Title = "Test Window";
    settings.WindowStartupLocation = WindowStartupLocation.Manual;
    settings.SizeToContent = SizeToContent.Manual;
    settings.Width = 450;
    settings.Height = 300;

    var TestViewModel new TestViewModel();
    WindowManagerFactory.WindowManager.ShowWindow(this.classSearch, null, settings);

Caliburn.Micro should again, resolve your Views to the correct ViewModels.

Hopefully there's something useful in there somewhere. I sort of arrived at this solution through a few design iterations, so this may not be the optimal approach to some of these problems. If anyone has any constructive criticism, please let me know =D

查看更多
登录 后发表回答