Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

AvalonDock and Caliburn Micro Screen Conductor

4.50/5 (2 votes)
22 Nov 2011CPOL1 min read 25.8K  
Using AvalonDock and Caliburn Micro Screen Conductor together

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:

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

C#
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);
                                        //TODO: can this be done by xaml caliburn.View
                                        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:

C#
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:

C#
//ispired from http://frankmao.com/2010/11/19/when-caliburn-micro-meets-avalondock/
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)