Click here to Skip to main content
15,881,204 members
Articles / Desktop Programming / WPF

AvalonDock and Caliburn Micro Screen Conductor

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
22 Nov 2011CPOL1 min read 25.1K   7   4
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)


Written By
Italy Italy
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDocumentPane not found in AvalonDock 2.0 Pin
I am Death11-Aug-16 19:33
I am Death11-Aug-16 19:33 
QuestionUpdate to latest AvalonDock? Pin
Daniele Fusi4-Nov-12 6:50
Daniele Fusi4-Nov-12 6:50 
QuestionView not updating the changes in properties of ViewModel Pin
md_aliyar26-May-12 22:33
md_aliyar26-May-12 22:33 
GeneralMy vote of 5 Pin
stooboo6-Dec-11 12:53
stooboo6-Dec-11 12:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.