Caliburn Micro library implements a screen conductor to handle multiple screen models with only one active, typically used for tabbed views, that is easy to implement by deriving your model from Conductor<IScreen>.Collection.OneActive
. This works out of the box with the standard tab control, but it is not possible to use it for example with the tabbed documents in AvalonDock. The only solution I found, that for some reason I will say below, is this one. I don’t like this solution because it forces to write code inside the view, that is not acceptable in a pure MVVM solution, so I preferred to insulate the code in an attached behavior. In addition, the presented solution will work correctly with the Activate
/Deactivate
/CanClose
strategy on each document. We just need to modify the View markup as in the example below:
<ad:DockingManager Grid.Row="1">
<ad:ResizingPanel>
<ad:DocumentPane b:UseConductor.DocumentConductor="{Binding}"/>
</ad:ResizingPanel>
</ad:DockingManager>
As you can see, we just added an attached property UseConductor.DocumentConductor
that we bind to the current model. Of course, the model is a OneActive
screen conductor. The behavior takes care to connect the document items of the DocumentPane
with the screen conductor items. If each screen implements IScreen
, the proper Activate
/Deactivate
/CanClose
are called, so we can even handle the case of canceling the close of a dirty document. Here is the attached behavior code:
public class UseConductor:DependencyObject
{
public static object GetDocumentConductor(DependencyObject obj)
{
return obj.GetValue(DocumentConductorProperty);
}
public static void SetDocumentConductor(DependencyObject obj, object value)
{
obj.SetValue(DocumentConductorProperty, value);
}
static Dictionary<DockingManager, ContentControl> previousActive =
new Dictionary<DockingManager, ContentControl>();
public static readonly DependencyProperty DocumentConductorProperty =
DependencyProperty.RegisterAttached("DocumentConductor",
typeof(object), typeof(UseConductor), new UIPropertyMetadata(null,
(depo, depa) =>
{
if (depo is DocumentPane)
{
var pane = depo as DocumentPane;
if (pane.GetManager() == null)
return;
pane.GetManager().ActiveDocumentChanged += (s, e) =>
{
var dm = s as DockingManager;
if (previousActive.ContainsKey(dm))
{
var prev = ViewModelLocator.LocateForView(
previousActive[dm].Content) as IScreen;
if (null != prev)
{
prev.Deactivate(false);
}
}
previousActive[dm] = pane.GetManager().ActiveDocument;
var current = ViewModelLocator.LocateForView(
pane.GetManager().ActiveDocument.Content) as IScreen;
if (null != current)
{
current.Activate();
}
};
var conductor = depa.NewValue as Conductor<IScreen>.Collection.OneActive;
conductor.Items.CollectionChanged += (s, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var screen in e.NewItems)
{
var view = LocateViewFor(screen);
var tabItem = new DocumentContent();
tabItem.Closing += (ss, ee) =>
{
var closingScreen = screen as IScreen;
if (null != closingScreen)
{
ee.Cancel = true;
closingScreen.CanClose((close)=>
ForceClose(pane,closingScreen,close,conductor)
);
}
};
ViewModelBinder.Bind(screen, tabItem, null);
tabItem.Content = view;
BindTabTitle(tabItem);
(depo as DocumentPane).Items.Add(tabItem);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var screen in e.OldItems)
{
foreach (ContentControl doc in (depo as DocumentPane).Items)
{
if (doc.Content == screen)
{
(depo as DocumentPane).Items.Remove(doc);
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
(depo as DocumentPane).Items.Clear();
break;
}
};
}
}
));
private static void ForceClose(DocumentPane pane,IScreen closingScreen,
bool close,Conductor<IScreen>.Collection.OneActive conductor)
{
if (close == true)
{
foreach (var d in pane.Items)
{
var screen = ViewModelLocator.LocateForView(d);
if (screen == closingScreen)
{
closingScreen.Deactivate(true);
pane.Items.Remove(d);
conductor.DeactivateItem(closingScreen,false);
conductor.Items.Remove(closingScreen);
break;
}
}
}
}
private static object LocateViewFor(object viewModel)
{
var view = ViewLocator.LocateForModelType(viewModel.GetType(), null, null);
return view;
}
static void BindTabTitle(DocumentContent tab)
{
DependencyProperty textProp = DocumentContent.TitleProperty;
Binding b = new Binding("DisplayName");
BindingOperations.SetBinding(tab, textProp, b);
}
}
An example MainModel
can be the following one:
public class MainViewModel:Conductor<IScreen>.Collection.OneActive
{
public void Loaded()
{
ActivateItem(new TabModel() { DisplayName = "Example 1" });
ActivateItem(new TabModel() { DisplayName = "Example 2" });
ActivateItem(new TabModel() { DisplayName = "Example 3" });
ActivateItem(new TabModel() { DisplayName = "Example 4" });
}
}
We just add some random document to see how it behaves.
And here is an example of a single screen model:
public class TabModel:Screen
{
protected override void OnActivate()
{
base.OnActivate();
}
protected override void OnDeactivate(bool close)
{
base.OnDeactivate(close);
}
public override void CanClose(Action<bool> callback)
{
if (MessageBox.Show("Do you really want to close" + DisplayName,
"Close ?", System.Windows.MessageBoxButton.OKCancel) ==
System.Windows.MessageBoxResult.OK)
{
callback(true);
}
}
}
So we have the conductor, without touching the View code and without creating a custom screen conductor.