Reputation: 357
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.
Upvotes: 2
Views: 2692
Reputation: 8656
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
Upvotes: 2
Reputation: 1501163
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.
Upvotes: 3