Click here to Skip to main content
Click here to Skip to main content
Go to top

AvalonDock and Caliburn Micro Screen Conductor

, 22 Nov 2011
Rate this:
Please Sign up or sign in to vote.
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:

<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);
                                        //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:

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:

//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)

Share

About the Author

No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberstooboo6-Dec-11 12:53 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140926.1 | Last Updated 22 Nov 2011
Article Copyright 2011 by Felice Pollano
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid