Click here to Skip to main content
15,891,316 members
Articles / Desktop Programming / WPF

AvalonDock and MVVM

Rate me:
Please Sign up or sign in to vote.
4.87/5 (50 votes)
9 Oct 2011CPOL34 min read 214.9K   9.2K   135  
Demonstrates a technique for integrating AvalonDock with an MVVM application.
using System;
using System.Windows.Controls;
using System.Windows;
using AvalonDock;
using System.Collections.Generic;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using System.ComponentModel;

namespace AvalonDockMVVM
{
    /// <summary>
    /// Interaction logic for AvalonDockHost.xaml
    /// </summary>
    public partial class AvalonDockHost : UserControl
    {
        #region Dependency Properties 

        public static readonly DependencyProperty PanesProperty =
            DependencyProperty.Register("Panes", typeof(IList), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(DocumentsOrPanes_PropertyChanged));

        public static readonly DependencyProperty DocumentsProperty =
            DependencyProperty.Register("Documents", typeof(IList), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(DocumentsOrPanes_PropertyChanged));

        public static readonly DependencyProperty ActiveDocumentProperty =
            DependencyProperty.Register("ActiveDocument", typeof(object), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ActiveDocumentOrPane_PropertyChanged));

        public static readonly DependencyProperty ActivePaneProperty =
            DependencyProperty.Register("ActivePane", typeof(object), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ActiveDocumentOrPane_PropertyChanged));

        //
        // Attached properties.
        //

        public static readonly DependencyProperty IsPaneVisibleProperty =
            DependencyProperty.RegisterAttached("IsPaneVisible", typeof(bool), typeof(AvalonDockHost),
                new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsPaneVisible_PropertyChanged));

        #endregion Dependency Properties

        #region Private Data Members

        /// <summary>
        /// A dictionary that maps from view-model object to Avalondock ManagedContent object.
        /// </summary>
        private Dictionary<object, ManagedContent> contentMap = new Dictionary<object, ManagedContent>();

        /// <summary>
        /// Set to 'true' to disable the request for user confirmation while closing a document.
        /// </summary>
        private bool disableClosingEvent = false;

        #endregion Private Data Members

        public AvalonDockHost()
        {
            InitializeComponent();

            //
            // Hook the AvalonDock event that is raised when the focused content is changed.
            //
            dockingManager.ActiveContentChanged += new EventHandler(dockingManager_ActiveContentChanged);

            UpdateActiveContent();
        }

        /// <summary>
        /// The collection of view-model objects for panes.
        /// Adding an object to this collection results in 
        /// a pane being added to AvalonDock.  There must be
        /// a DataTemplate for the view-model defined within
        /// the resources of the visual-tree and the 
        /// DataTemplate should contain an AvalonDock
        /// DockableControl.
        /// NOTE: This property is initially null, you should 
        /// either assign a collection to it or, as it is
        /// intended to be used, data-bind it to a collection
        /// in the view-model.
        /// </summary>
        public IList Panes
        {
            get
            {
                return (IList)GetValue(PanesProperty);
            }
            set
            {
                SetValue(PanesProperty, value);
            }
        }

        /// <summary>
        /// The collection of view-model objects for documents.
        /// Adding an object to this collection results in 
        /// a document being added to AvalonDock.  There must be
        /// a DataTemplate for the view-model defined within
        /// the resources of the visual-tree and the 
        /// DataTemplate should contain an AvalonDock
        /// DocumentControl.
        /// NOTE: This property is initially null, you should 
        /// either assign a collection to it or, as it is
        /// intended to be used, data-bind it to a collection
        /// in the view-model.
        /// </summary>
        public IList Documents
        {
            get
            {
                return (IList)GetValue(DocumentsProperty);
            }
            set
            {
                SetValue(DocumentsProperty, value);
            }
        }

        /// <summary>
        /// The view-model object for the currently active document.
        /// This can programatically set to change the active document,
        /// or it can be data-bound to the view-model so that the active
        /// document can be changed via the view-model.
        /// It is also used to retieve the currently active document's view-model
        /// and is automatically updated when the user interacts directly with
        /// AvalonDock selecting a tabbed-document.
        /// </summary>
        public object ActiveDocument
        {
            get
            {
                return (object)GetValue(ActiveDocumentProperty);
            }
            set
            {
                SetValue(ActiveDocumentProperty, value);
            }
        }

        /// <summary>
        /// The view-model object for the currently active pane.
        /// This can programatically set to change the active pane,
        /// or it can be data-bound to the view-model so that the active
        /// pane can be changed via the view-model.
        /// It is also used to retieve the currently active pane's view-model
        /// and is automatically updated when the user interacts directly with
        /// AvalonDock selecting a pane.
        /// </summary>
        public object ActivePane
        {
            get
            {
                return (object)GetValue(ActivePaneProperty);
            }
            set
            {
                SetValue(ActivePaneProperty, value);
            }
        }

        /// <summary>
        /// Allow access to the Avalondock DockingManager.
        /// This class isn't a complete wrapper, 
        /// it should be considered a helper that provides MVVM support for Avalondock.
        /// </summary>
        public DockingManager DockingManager
        {
            get
            {
                return dockingManager;
            }
        }

        /// <summary>
        /// Event raised when Avalondock has loaded.
        /// </summary>
        public event EventHandler<EventArgs> AvalonDockLoaded;

        /// <summary>
        /// Event raised when a document is being closed by clicking on the AvalonDock X button .
        /// </summary>
        public event EventHandler<DocumentClosingEventArgs> DocumentClosing;

        /// <summary>
        /// Sets the IsPaneVisible for an element.
        /// </summary>
        public static void SetIsPaneVisible(UIElement element, bool value)
        {
            element.SetValue(IsPaneVisibleProperty, value);
        }

        /// <summary>
        /// Gets the IsPaneVisible property for an element.
        /// </summary>
        public static bool GetIsPaneVisible(UIElement element)
        {
            return (bool)element.GetValue(IsPaneVisibleProperty);
        }

        #region Private Methods

        /// <summary>
        /// Event raised when the Documents or Panes properties have changed.
        /// </summary>
        private static void DocumentsOrPanes_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (AvalonDockHost)d;

            //
            // Deal with the previous value of the property.
            //
            if (e.OldValue != null)
            {
                //
                // Remove the old panels from AvalonDock.
                //
                var oldPanels = (IList)e.OldValue;
                c.RemovePanels(oldPanels);

                var observableCollection = oldPanels as INotifyCollectionChanged;
                if (observableCollection != null)
                {
                    //
                    // Unhook the CollectionChanged event, we no longer need to receive notifications
                    // of modifications to the collection.
                    //
                    observableCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(c.documentsOrPanes_CollectionChanged);
                }
            }

            //
            // Deal with the new value of the property.
            //
            if (e.NewValue != null)
            {
                //
                // Add the new panels to AvalonDock.
                //
                var newPanels = (IList)e.NewValue;
                c.AddPanels(newPanels);

                var observableCollection = newPanels as INotifyCollectionChanged;
                if (observableCollection != null)
                {
                    //
                    // Hook the CollectionChanged event to receive notifications
                    // of future modifications to the collection.
                    //
                    observableCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(c.documentsOrPanes_CollectionChanged);
                }
            }
        }

        /// <summary>
        /// Event raised when the 'Documents' or 'Panes' collection have had items added/removed.
        /// </summary>
        private void documentsOrPanes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                //
                // The collection has been cleared, need to remove all
                // documents or panes from AvalonDock depending on which collection was
                // actually cleared.
                //
                ResetDocumentsOrPanes(sender);
            }
            else
            {
                if (e.OldItems != null)
                {
                    //
                    // Remove the old panels from AvalonDock.
                    //
                    RemovePanels(e.OldItems);
                }

                if (e.NewItems != null)
                {
                    //
                    // Add the new panels to AvalonDock.
                    //
                    AddPanels(e.NewItems);
                }
            }
        }

        /// <summary>
        /// This method resets documents or panes in response
        /// to the Reset action of the CollectionChanged events.
        /// </summary>
        private void ResetDocumentsOrPanes(object sender)
        {
            if (sender == this.Documents)
            {
                //
                // We are clearing the collection of documents.
                //
                foreach (var item in this.contentMap.ToArray())
                {
                    if (item.Value is DocumentContent)
                    {
                        //
                        // Only remove DocumentContent components.
                        //
                        RemovePanel(item.Key);
                    }
                }
            }
            else if (sender == this.Panes)
            {
                //
                // We are clearing the collection of panes.
                //
                foreach (var item in this.contentMap.ToArray())
                {
                    if (item.Value is DockableContent)
                    {
                        //
                        // Only remove DockableContent components.
                        //
                        RemovePanel(item.Key);
                    }
                }
            }
            else
            {
                throw new ApplicationException("Unexpected CollectionChanged event sender: " + sender.GetType().Name);
            }
        }

        /// <summary>
        /// Add panels to Avalondock.
        /// </summary>
        private void AddPanels(IList panels)
        {
            foreach (var panel in panels)
            {
                AddPanel(panel);
            }
        }

        /// <summary>
        /// Add a panel to Avalondock.
        /// </summary>
        private void AddPanel(object panel)
        {
            //
            // Instantiate a UI element based on the panel's view-model type.
            // The visual-tree is searched to find a DataTemplate keyed to the requested type.
            //
            var panelViewModelType = panel.GetType();
            var uiElement = DataTemplateUtils.InstanceTemplate(panelViewModelType, this, panel);
            if (uiElement == null)
            {
                throw new ApplicationException("Failed to find data-template for type: " + panel.GetType().Name);
            }

            //
            // Cast the instantiated UI element to an AvalonDock ManagedContent.
            // ManagedContent can refer to either an AvalonDock DocumentContent
            // or an AvalonDock DockableContent.
            //
            var managedContent = uiElement as ManagedContent;
            if (managedContent == null)
            {
                throw new ApplicationException("Found data-template for type: " + panel.GetType().Name + ", but the UI element generated is not a ManagedContent (base-class of DocumentContent/DockableContent), rather it is a " + uiElement.GetType().Name);
            }

            //
            // Associate the panel's view-model with the Avalondock ManagedContent so it can be retrieved later.
            //
            contentMap[panel] = managedContent;

            //
            // Hook the event to track when the document has been closed.
            //
            managedContent.Closed += new EventHandler(managedContent_Closed);

		    var documentContent = managedContent as DocumentContent;
		    if (documentContent != null)
		    {
			    //
			    // For documents only, hook Closing so that the application can be informed
			    // when a document is in the process of being closed by the use clicking the
			    // AvalonDock close button.
			    //
			    documentContent.Closing += new EventHandler<CancelEventArgs>(documentContent_Closing);
		    }
		    else
		    {
			    var dockableContent = managedContent as DockableContent;
			    if (dockableContent != null)
			    {
				    //
				    // For panes only, hook StateChanged so we know when a DockableContent is shown/hidden.
				    //
				    dockableContent.StateChanged += new RoutedEventHandler(dockableContent_StateChanged);
			    }
			    else
			    {
                    throw new ApplicationException("Panel " + managedContent.GetType().Name + " is expected to be either DocumentContent or DockableContent."); 
			    }
		    }

            managedContent.Show(dockingManager);
            managedContent.Activate();
        }

        /// <summary>
        /// Remove panels from Avalondock.
        /// </summary>
        private void RemovePanels(IList panels)
        {
            foreach (var panel in panels)
            {
                RemovePanel(panel);
            }
        }

        /// <summary>
        /// Remove a panel from Avalondock.
        /// </summary>
        private void RemovePanel(object panel)
        {
            //
            // Look up the document in the content map.
            //
            ManagedContent managedContent = null;
            if (contentMap.TryGetValue(panel, out managedContent))
            {
                disableClosingEvent = true;

                try
                {
                    //
                    // The content was still in the map, and therefore still open, so close it.
                    //
                    managedContent.Close();
                }
                finally
                {
                    disableClosingEvent = false;
                }
            }
        }

        /// <summary>
        /// Event raised when an AvalonDock DocumentContent is being closed.
        /// </summary>
        private void documentContent_Closing(object sender, CancelEventArgs e)
        {
            var documentContent = (DocumentContent)sender;
            var document = documentContent.DataContext;

            if (!disableClosingEvent)
            {
                if (this.DocumentClosing != null)
                {
                    //
                    // Notify the application that the document is being closed.
                    //
                    var eventArgs = new DocumentClosingEventArgs(document);
                    this.DocumentClosing(this, eventArgs);

                    if (eventArgs.Cancel)
                    {
                        //
                        // Closing of the document is to be cancelled.
                        //
                        e.Cancel = true;
                        return;
                    }
                }
            }

            documentContent.Closing -= new EventHandler<CancelEventArgs>(documentContent_Closing);
        }

        /// <summary>
        /// Event raised when an Avalondock ManagedContent has been closed.
        /// </summary>
        private void managedContent_Closed(object sender, EventArgs e)
        {
            var managedContent = (ManagedContent)sender;
            var content = managedContent.DataContext;

            //
            // Remove the content from the content map right now.
            // There is no need to keep it around any longer.
            //
            contentMap.Remove(content);

            managedContent.Closed -= new EventHandler(managedContent_Closed);

            var documentContent = managedContent as DocumentContent;
            if (documentContent != null)
            {
                this.Documents.Remove(content);

                if (this.ActiveDocument == content)
                {
                    //
                    // Active document has closed, clear it.
                    //
                    this.ActiveDocument = null;
                }
            }
            else
            {
                var dockableContent = managedContent as DockableContent;
                if (dockableContent != null)
                {
                    //
                    // For panes only, unhook StateChanged event.
                    //
                    dockableContent.StateChanged -= new RoutedEventHandler(dockableContent_StateChanged);

                    this.Panes.Remove(content);

                    if (this.ActivePane == content)
                    {
                        //
                        // Active pane has closed, clear it.
                        //
                        this.ActivePane = null;
                    }
                }
            }
        }

        /// <summary>
        /// Event raised when the 'dockable state' of a DockableContent has changed.
        /// </summary>
        private void dockableContent_StateChanged(object sender, RoutedEventArgs e)
        {
            var dockableContent = (DockableContent)sender;
            SetIsPaneVisible(dockableContent, dockableContent.State != DockableContentState.Hidden);
        }

        /// <summary>
        /// Update the active pane and document from the currently active AvalonDock component.
        /// </summary>
        private void UpdateActiveContent()
        {
            var activePane = dockingManager.ActiveContent as DockableContent;
            if (activePane != null)
            {
                //
                // Set the active document so that we can bind to it.
                //
                this.ActivePane = activePane.DataContext;
            }
            else
            {
                var activeDocument = dockingManager.ActiveContent as DocumentContent;
                if (activeDocument != null)
                {
                    //
                    // Set the active document so that we can bind to it.
                    //
                    this.ActiveDocument = activeDocument.DataContext;
                }
            }
        }

        /// <summary>
        /// Event raised when the IsPaneVisible property changes.
        /// </summary>
        private static void IsPaneVisible_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var avalonDockContent = o as ManagedContent;
            if (avalonDockContent != null)
            {
                bool isVisible = (bool)e.NewValue;
                if (isVisible)
                {
                    avalonDockContent.Show();
                }
                else
                {
                    avalonDockContent.Hide();
                }
            }
        }

        /// <summary>
        /// Event raised when the active content has changed.
        /// </summary>
        private void dockingManager_ActiveContentChanged(object sender, EventArgs e)
        {
            UpdateActiveContent();
        }

        /// <summary>
        /// Event raised when the ActiveDocument or ActivePane property has changed.
        /// </summary>
        private static void ActiveDocumentOrPane_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var c = (AvalonDockHost)d;
            ManagedContent managedContent = null;
            if (e.NewValue != null &&
                c.contentMap.TryGetValue(e.NewValue, out managedContent))
            {
                managedContent.Activate();
            }
        }

        /// <summary>
        /// Event raised when Avalondock has loaded.
        /// </summary>
        private void AvalonDock_Loaded(object sender, RoutedEventArgs e)
        {
            if (AvalonDockLoaded != null)
            {
                AvalonDockLoaded(this, EventArgs.Empty);
            }
        }

        #endregion Private Methods
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Chief Technology Officer
Australia Australia
Software craftsman | Author | Writing rapidfullstackdevelopment.com - Posting about how to survive and flourish as a software developer

Follow on Twitter for news and updates: https://twitter.com/codecapers

I'm writing a new book: Rapid Fullstack Development. Learn from my years of experience and become a better developer.

My second book, Bootstrapping Microservices, is a practical and project-based guide to building distributed applications with microservices.

My first book Data Wrangling with JavaScript is a comprehensive overview of working with data in JavaScript.

Data-Forge Notebook is my notebook-style application for data transformation, analysis and transformation in JavaScript.

I have a long history in software development with many years in apps, web apps, backends, serious games, simulations and VR. Making technology work for business is what I do: building bespoke software solutions that span multiple platforms.

I have years of experience managing development teams, preparing technical strategies and creation of software products. I can explain complicated technology to senior management. I have delivered cutting-edge products in fast-paced and high-pressure environments. I know how to focus and prioritize to get the important things done.

Author

- Rapid Fullstack Development
- Bootstrapping Microservices
- Data Wrangling with JavaScript

Creator of Market Wizard

- https://www.market-wizard.com.au/

Creator of Data-Forge and Data-Forge Notebook

- http://www.data-forge-js.com
- http://www.data-forge-notebook.com

Web

- www.codecapers.com.au

Open source

- https://github.com/ashleydavis
- https://github.com/data-forge
- https://github.com/data-forge-notebook


Skills

- Quickly building MVPs for startups
- Understanding how to get the most out of technology for business
- Developing technical strategies
- Management and coaching of teams & projects
- Microservices, devops, mobile and fullstack software development

Comments and Discussions