When Caliburn.Micro meets AvalonDock

Using classic WPF tabcontrol in Caliburn.Micro is very easy, view model just needs to inherited from Conductor<IScreen>.Collection.OneActive, then add sub-viewmodels into Items property, or call ActivateItem(). The view side only need to name the tablecontrol to “Items”, everything is done! (WPF only, SilverLight app needs to implement tabitemconverter explicitly)

But, in some fancy used user control world, e.g., AvalonDock, this doesn’t work anymore, because Caliburn magic just automatically find those mapping views and then convert them to tabitems, it doesn’t know Avalon Control at all.

Sure, we can create our own TabItemConverter.


   public class TabItemConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is BindableCollection<IScreen>)
            {
                var screenList = (BindableCollection<IScreen>)value;

                List<DocumentContent> result = new List<DocumentContent>();

                foreach (var screen in screenList)
                {
                    var view = LocateViewFor(screen);

                    var tabItem = new DocumentContent();
                    ViewModelBinder.Bind(screen, tabItem, null);
                    tabItem.Content = view; //TODO: can this be done by xaml caliburn.View
                    BindTabTitle(tabItem);
                    result.Add(tabItem);

                }
                return result;

            }

            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        private object LocateViewFor(object viewModel)
        {
            var view = ViewLocator.LocateForModelType(viewModel.GetType(), null, null);
            return view;
        }

        static void BindTabTitle(DocumentContent tab)
        {
            DependencyProperty textProp = DocumentContent.TitleProperty;
//            if (!BindingOperations.IsDataBound(tab, textProp)) // Not available in Silverlight
            {
                Binding b = new Binding("DisplayName");
                BindingOperations.SetBinding(tab, textProp, b);
            }
        }


                <ad:DocumentPane ItemsSource="{Binding Items, Converter={StaticResource TabItemConverter}}" SelectedItem="{Binding ActiveItem, Mode=TwoWay}" >

                </ad:DocumentPane>

It works, sort of. Opening tab looks fine, but active doesn’t. Any still have some goofy issues, like when click the dropdown icon at the upper right corner will kill the app!?It probably because the ItemsSource binding doesn’t work very well with AvalonDock control.

I created the workaround for Caliburn + Avalondock, now with the new slim but powerful Caliburn.Micro framewrok, this task is much easier, thanks to the new static classes: ViewLocator and ViewModelBinder.

Details: it was a problem in the pure view based design world. In shellView, we can easily do this:


            var dockableContent = new DockableContent
                                      {
                                          Content = new SearchView(),
                                      };

            dockManager.Show(dockableContent, DockableContentState.Docked, AnchorStyle.Left);

The idea of view model based design is to remove the new SearchView(), instead should rely completely on shellViewModel to handle all the creation, by calling ActivateItem(searchViewModel).

The problem is, AvalonDock doesn’t behave as the classic tabcontrol, we can not just bind the items into it’s datacontext.

The solution, use event handler between view and view model:

Code:


   public class ShellViewModel : Conductor<Screen>, IShellViewModel
    {
        private readonly IMessageBox _messageBox;
        private IShellView _view;

        public ShellViewModel(IMessageBox messageBox)
        {
            _messageBox = messageBox;
            RequestViewToLoadDockPanel = (o, e) => { };
        }

        protected override void OnViewLoaded(object view)
        {
            _view = (IShellView) view;
            RequestViewToLoadDockPanel += _view.LoadDockPanel;

            ShowLeftDockPanel();
        }

        private void ShowLeftDockPanel()
        {
            var searchViewModel = new SearchViewModel(_messageBox);
            ActivateItem(searchViewModel);
            RequestViewToLoadDockPanel(this, new ScreenEventArgs(searchViewModel));
        }

        public event EventHandler<ScreenEventArgs> RequestViewToLoadDockPanel;
    }

    public interface IShellView
    {
        void LoadDockPanel(object sender, ScreenEventArgs e);
    }

    public class ScreenEventArgs : EventArgs
    {
        public Screen Screen { get; set; }

        public ScreenEventArgs(Screen screen)
        {
            Screen = screen;
        }
    }

    public partial class ShellView : Window, IShellView
    {
        public ShellView()
        {
            InitializeComponent();
        }

        #region IShellView Members

        public void LoadDockPanel(object sender, ScreenEventArgs e)
        {
            var view =   LocateViewFor(e.Screen);

            var dockableContent = new DockableContent
                                      {
                                          Content = view,
                                      };

            dockManager.Show(dockableContent, DockableContentState.Docked, AnchorStyle.Left);
        }

        #endregion

        private object LocateViewFor(object viewModel)
        {
            var view = ViewLocator.LocateForModel(viewModel, null, null);

            ViewModelBinder.Bind(viewModel, view, null);

            return view;
        }
    }
About these ads

5 thoughts on “When Caliburn.Micro meets AvalonDock

  1. Pingback: Implement dirty notification on avalon doc tab + caliburn micro « FrankMao.com

  2. ViewModel (can be done in a custom conductor):

    readonly BindableCollection items = new BindableCollection();

    public void ShowContent() {
    ContentViewModel model = new ContentViewModel();
    DockableContent dockableContent = new DockableContent();
    dockableContent.Content = ViewLocator.LocateForModel(model, null, null);
    this.items.Add(dockableContent);
    }

    and with a custom convention for the element:

    ConventionManager.AddElementConvention(DocumentPane.ItemsSourceProperty, “ItemsSource”, “SelectionChanged”)
    .ApplyBinding = (viewModelType, path, property, element, convention) => {
    if(!ConventionManager.SetBinding(viewModelType, path, property, element, convention))
    return false;

    DocumentPane content = (DocumentPane)element;
    //customize it, eg …
    return true;
    };

    and you can use the 2 way binding between the viewmodel and the view, where the
    x:Name of the ad:DocumentPane is bound to the public property of the viewmodel

    grtz

  3. Pingback: Felice Pollano Blog - AvalonDock and Caliburn Micro Screen Conductor

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s