Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

AvalonDock and MVVM

0.00/5 (No votes)
9 Oct 2011 3  
Demonstrates a technique for integrating AvalonDock with an MVVM application.

Sample Code

Sample Code

Contents

Introduction

Do you use or want to use AvalonDock?

Do you want to use it with MVVM?

In this article I demonstrate one way of adapting AvalonDock for use in an MVVM application.

The sample application I have developed for the article is a simple text editor. The core code for the technique is in a class that I have named AvalonDockHost. This class is in its own project and can be reused in other AvalonDock applications.

AvalonDockHost wraps AvalonDock and is an adapter that allows it to live in relative peace with the rest of your MVVM application. It is important though to note that AvalonDockHost is not intended to be a complete wrapper for AvalonDock and it is not really intended to make AvalonDock easier to use. I do believe however that the ultimate result is an easier to use AvalonDock, but only in the way that MVVM often makes things easier, but usually only if you already know MVVM and the underlying technology. So put more simply to get the most out of this it will help if you already know at least the basics of using AvalonDock.

This article is composed of three parts. The first is a walkthrough of the sample application and describes how to use AvalonDockHost. The second part discusses the implementation of AvalonDockHost. Please feel free to skip part 2 if you are not interested in the implementation. The third part is a short reference for AvalonDockHost.

Screenshot

This screenshot shows the sample application. The tabbed-document area contains the multiple opened text files. The pane to the top right is a list of currently open documents. The pane to the bottom right is a simple overview of the currently active document.

Assumed Knowledge

It is assumed that you know C# and have at least a basic grasp of WPF and MVVM.

I also assume that you understand the basic workings and functionality of AvalonDock. To get up to speed I suggest you read the AvalonDock getting started tutorial.

It seems too obvious to mention but I'll say it anyway. If you don't know what AvalonDock or MVVM is, or indeed if you do know of these technologies but don't know how to use them, then this article may not be what you are looking for. This article isn't intended to be a tutorial about using AvalonDock or MVVM, it is an article about how to merge the two.

Background

I have been using AvalonDock for a number of years now and I think that generally it is really great, however out-of-the-box it doesn't support MVVM. Although they probably have good reasons for this I really wanted to be able to use AvalonDock with MVVM!

In most of the time that I have used AvalonDock I have used it in a non-MVVM style. I can't say I was exactly unhappy with this state of affairs but I did for sometime have an itch that wanted to be scratched.

There are of course alternatives to AvalonDock and some existing AvalonDock wrappers. However after some research and experimenting I couldn't find anything I was happy with and so I set about writing my own AvalonDock MVVM wrapper. I also felt that the community was lacking a clear and simple account of how to use AvalonDock with MVVM.

I was pleasantly surprised, and I hope after reading this article that you will agree, that it isn't actually as difficult as it might first seem. After a few hours of experimenting and figuring out I had written the first draft of the code for this article.

Aims and Concepts

I'll be demonstrating the AvalonDock MVVM technique with a simple text editor. As you read the article please bear in mind that the sample application is really only a toy and is only as sophisticated as necessary. The main aim is to demonstrate the use of AvalonDock in an MVVM application.

I want to add view-models for documents and panes to my application's view-model and have the appropriate AvalonDock components automatically instantiated. I also want to be able to indirectly manipulate AvalonDock components via my view-model. Setting the active document/pane and showing/hidding panes programatically are some examples of what I want to be able to do.

AvalonDockHost is the class at the core of this technique. It is a WPF user control whose purpose is to adapt AvalonDock to MVVM and allows documents and panes to be reflected in the application's view-model. Like any good WPF reusable control AvalonDockHost will make few assumptions about the structure of the application's view-model.

Lastly I want AvalonDockHost to have minimal dependencies. As it stands it only depends on AvalonDock and the .Net framework. I felt this was quite important because existing solutions to this problem often come packaged with additional code, libraries and bloat that I don't need and don't want. With the code for this article I don't require that you use any other components, this means you are free to integrate whichever other components you want, such as MVVM frameworks or extensibility frameworks and if you do indeed do this it would be great to hear from you what the experience was like.

Sample Application Walkthrough

The Solution and Projects

Extract AvalonDockMVVMSampleCode.zip and open AvalonDockMVVMSample.2008.sln in Visual Studio (I am still using VS 2008, please use AvalonDockMVVMSample.2010.sln if you are using VS 2010).

The solution contains the following projects:

The main class, AvalonDockHost, can be found in the AvalonDockMVVM project. I'll discuss the internals of AvalonDockHost in some detail in the Implementation section. In the walkthrough I will only discuss how AvalonDockHost is used.

The SampleApp project contains the application, the main window and the views for the panes.

The ViewModels project contains all the view-model classes for the application.

Running the Sample Application

You should start by running the sample app and exploring its functionality. The sample app has the features you might expect of a simple text editor: creating, opening, saving and closing documents. The View menu allows the panes to be hidden and shown.

The following annotated screenshot shows the View menu and labels the various components of the application.

Documents are created and opened in the central tabbed-document area. To the right of the tabbed-documents area are two dockable panes. The top pane is a list of currently open documents. The bottom pane is a simplistic overview of the currently active document. I collectively refer to documents and panes as panels.

As you would expect with AvalonDock the panels can be detached from the main window and left as floating windows or redocked at different locations in the main window. This allows the user to customize the layout of the application.

AvalonDockHost, Views and the View-model

The sample application has three distinct view classes: MainWindow, OpenDocumentsPaneView and DocumentOverviewPaneView. There is a third view that doesn't have a distinct class. The view for a text file document is so simple as to only contain a WPF TextBox and I have inlined the view as a DataTemplate within MainWindow.xaml. The ViewModels project contains the view-model classes for each of the views.

Here is an (simplified) overview of the classes (thanks to StarUML):

The solid lines indicate derivation (class hierarchy) and the dashed lines indicate dependency or usage.

MainWindowViewModel has Documents and Panes properties that are the collections of document and pane view-models. These properties are data-bound to AvalonDockHost's Documents and Panes properties in MainWindow.xaml:

    <AvalonDockMVVM:AvalonDockHost
        x:Name="avalonDockHost"
        Panes="{Binding Panes}"
        Documents="{Binding Documents}"
        ...
        />

With these data-bindings in place adding a document to AvalonDock is now as easy as adding to the Documents collection in the view-model. Similarly, a pane is added to AvalonDock by adding to the Panes collection. Actually we aren't quite there yet, because we still need to understand how AvalonDockHost transforms a panel view-model into an appropriate AvalonDock component and we will come to that soon.

In MainWindow.xaml there are additional data-bindings for ActiveDocument and ActivePane:

    <AvalonDockMVVM:AvalonDockHost
        ...
        ActiveDocument="{Binding ActiveDocument}"
        ActivePane="{Binding ActivePane}"
        ...
        />

ActiveDocument is set to the view-model for the active document and ActivePane is set to the view-model for the active pane. These properties can be set via the view-model and the change is propagated to AvalonDockHost via the data-binding which causes AvalonDockHost to activate and focus the specified AvalonDock component. These properties also reflect AvalonDock's current internal state. When the user interacts directly with AvalonDock (bypassing our view-model) to select and active a panel AvalonDockHost is notified and sets either ActiveDocument or ActivePane as appropriate. This change is then propagated back to the view-model via the data-binding.

Now that we have looked at the bindings between AvalonDockHost and the view-model here is a diagram that summarizes the relationships:

I often think of the view-model for an application as being a view-model tree. This is certainly the case in the sample application although the view-model tree here is not particularly deep. Other more complicated applications have many more levels in the tree. The root of the tree is the main window's view-model: MainWindowViewModel. At the next level down in the tree are the view-models for the various panels.

The following instantiation diagram shows the (simplified) view-model tree when documents are opened as seen in the earlier screenshot. View-model objects are in blue, their associated views are in purple.

The next image shows an annotated screenshot of the visual-tree for the same run of the sample application and shows where AvalonDockHost and the other views fit in.

To see how the view-model tree is instantiated let's look at MainWindowViewModel's constructor. First it saves a reference to the passed in IDialogProvider interface:

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        this.DialogProvider = dialogProvider;
        
        // ... rest of method ... 
    }

IDialogProvider is my way of indirectly providing view-dependent services to the view-model. It allows the view-model to invoke open and save file dialogs and to report error messages. It is also used to bring up a dialog box to request user confirmation when a modified file is being closed.

Next the view-models for the panes are created:

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        // ... save dialogProvider reference ...

        //
        // Initialize the 'Document Overview' pane view-model.
        //
        this.DocumentOverviewPaneViewModel = new DocumentOverviewPaneViewModel(this);

        //
        // Initialize the 'Open Documents' pane view-model.
        //
        this.OpenDocumentsPaneViewModel = new OpenDocumentsPaneViewModel(this);

        // ... rest of method ... 
    }

Note that a reference to MainWindowViewModel is passed into the constructors for each of the pane view-models. This allows them access to various services such as retreiving the list of open documents and the currently active document. This is a dependency that you might want to break in the design for a real text editor, maybe the main window's view-model should provide its services to the sub-view-models using an interface, but for my purposes this works and is simple.

Next the pane view-models are added to the Panes collection:

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        // ... save dialogProvider reference ...

        // ... instantiate pane view-models ...

        //
        // Add view-models for panes to the 'Panes' collection.
        //
        this.Panes = new ObservableCollection<abstractpaneviewmodel>();
        this.Panes.Add(this.DocumentOverviewPaneViewModel);
        this.Panes.Add(this.OpenDocumentsPaneViewModel);

        // ... rest of method ... 
    }

As the view-model Panes collection is data-bound to AvalonDockHost's Panes collection these view-models are subsequently pushed through to AvalonDockHost where they are transformed into appropriate AvalonDock components.

At the end of the constructor a sample text file document is instantiated and added to the Documents collection. Adding the document view-model to the Documents collection causes it to then be transformed into an AvalonDock component:

    public MainWindowViewModel(IDialogProvider dialogProvider)
    {
        // ... save dialogProvider reference ...

        // ... instantiate pane view-models ...
        
        // ... add pane view-models to Panes collection ...

        //
        // Add an example/test document view-model.
        //
        this.Documents = new ObservableCollection<textfiledocumentviewmodel>();
        this.Documents.Add(new TextFileDocumentViewModel(string.Empty, "test data!", true));
    }

MainWindowViewModel contains methods that implement the text editor's features. Methods such as NewFile, OpenFile and SaveFile that are ultimately invoked by commands defined in MainWindow.xaml. I won't explain most of these functions as they are all quite short and easy to read. The important thing to note in reading these functions is that adding a document to AvalonDock is achieved by adding the document's view-model to the Documents collection. Closing a document is achieved by removing the document's view-model. Adding and removing panes is similar and just as easy, however it is the dynamic addition and removal of documents that happens most often during a session of a text editor and is the most interesting.

I will now examine what happens when a document is closed after the user has clicked on the AvalonDock document close button.

Besides clicking the AvalonDock document close button there are several other ways to close a document. You can choose Close or Close All from the File menu. Also exiting the application implicitly causes all documents to be closed.

When the user closes a document (or all documents) via the menu or when they exit the application a view-model method is invoked that removes the document from the Documents collection, thus closing it. When a modified document is being closed the user is first queried to confirm the closing of the modified document. The document is only closed if the user has approved the action.

However when the AvalonDock document close button has been clicked things happen differently. In this case the user is interacting directly with AvalonDock and the view-model is normally by-passed. To notify the application of a document that is closed in this way AvalonDockHost raises the DocumentClosing event.

The sample application handles this event:

    <AvalonDockMVVM:AvalonDockHost
        ...
        DocumentClosing="avalonDockHost_DocumentClosing"
        />

The event-handler invokes the document closing logic in the view-model:

    /// <summary>
    /// Event raised when a document is being closed by clicking the 'X' button in AvalonDock.
    /// </summary>
    private void avalonDockHost_DocumentClosing(object sender, DocumentClosingEventArgs e)
    {
        var document = (TextFileDocumentViewModel)e.Document;
        if (!this.ViewModel.QueryCanCloseFile(document))
        {
            e.Cancel = true;
        }
    }

It is important to reiterate that the DocumentClosing event is only raised when a document is being closed after the user has clicked the AvalonDock document close button. It is not raised when the view-model removes a document from the Documents collection. This is logical because when the view-model directly removes a document, the view-model is already aware that a document is being closed and so does not need to be notified. However when a document is closed because of direct user interaction with AvalonDock the view-model has no other way of knowing that a document is being closed other than by the DocumentClosing event.

When the application handles DocumentClosing the document is in the process of being closed but has not actually been closed. You can see from the previous code snippet that it is possible to cancel the close operation and prevent the document from being closed. After the DocumentClosing event has finished and provided the close operation has not been cancelled the document is then closed and AvalonDockHost itself removes the document's view-model from the Documents collection.

DataTemplates for AvalonDock Documents and Panes

Near the start of MainWindow.xaml, after the declarations of the routed commands, are the data-templates that are used to transform panel view-models into appropriate AvalonDock components. The sample application has three such data-templates: one for text file documents and one for each of the two panes.

The DataType for each of the data-templates is set to the relevant view-model class. When view-models are added to AvalonDockHost's Documents or Panes collections the visual-tree is searched for matching data-templates in a manner that is very similar to the usual WPF mechansim for associating a view-model with a DataTemplate. When AvalonDockHost transforms a view-model into a UI element it expects the root element to be an AvalonDock component. This is why all the data-templates have DocumentContent or DockableContent as the root element. Using another type of UI element as the root element will result in an exception being thrown.

As a first example you can see that TextFileDocumentViewModel's data-template contains an AvalonDock DocumentContent with an embedded WPF TextBox:

    <!-- 
    Data template for displaying tabbed documents.
    This is really simple they are just represented by an AvalonDock
    DocumentContent that contains a simple WPF TextBox.
    -->           
    <DataTemplate
        DataType="{x:Type ViewModels:TextFileDocumentViewModel}"
        >
        <ad:DocumentContent
            Title="{Binding Title}"      
            ToolTip="{Binding ToolTip}"
            >                
            <TextBox
                Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
                />
        </ad:DocumentContent>
    </DataTemplate>

You might now be wondering why it is so important that an AvalonDock component is required as the data-template's root element. After all AvalonDockHost has separate collections for Documents and Panes, it therefore knows the difference between documents and panes, why can't it automatically create an appropriate AvalonDock component instead of making the programmer specify it in the data-template? It is important for one reason. It allows the properties of the AvalonDock component to be explicitly set or data-bound to the view-model. For instance in the code snippet above Title and ToolTip are data-bound to view-model properties.

You could take this further if you were inclined and create your own wrappers for the AvalonDock components to use in these data-templates. Then you would be almost completely abstracted from AvalonDock. However a complete abstraction is not my purpose here and personally I think it would take more time than is worth investing.

In the data-template above it is easy to see why there is no explicit view for a text file document. The 'view' only contains a TextBox so it seems like overkill to create a separate user control. Instead I have declared the 'view' inline within the main window. The TextBox's Text property is data-bound to the Text property in the document's view-model and UpdateSourceTrigger is set to PropertyChanged so that the view-model's text is updated whenever the user changes the text in the text box. To be sure this is not very efficient but it keeps the sample code simple.

The data-templates for the panes are also declared in MainWindow.xaml, however unlike the view for the document view-model, the views for the pane view-models are delegated to separate user controls.

As an example I show only the data-template for OpenDocumentsPaneViewModel. The view for DocumentOverviewPaneViewModel is very similar so I leave you to look at that yourself. This data-template contains an AvalonDock DockableContent as the root element. Embedded within that is an instance of the OpenDocumentsPaneView user control:

    <!--
    The DataTemplate for the 'Open Documents' pane.
    This uses an AvalonDock DockableContent that contains an instance of the
    OpenDocumentPaneView user control.
    -->
    <DataTemplate
        DataType="{x:Type ViewModels:OpenDocumentsPaneViewModel}"
        >
        <ad:DockableContent
            x:Name="openDocumentsPane"
            Title="Open Documents"
            AvalonDockMVVM:AvalonDockHost.IsPaneVisible="{Binding IsVisible}"
            >
            <local:OpenDocumentsPaneView />
        </ad:DockableContent>
    </DataTemplate>

It is very important to give a name to each pane that is defined, for instance here it is named openDocumentsPane. These names are required for AvalonDock layout to be saved and restored. Without them you will not be able to persist custom user-layout between application sessions.

Note the data-binding of the IsPaneVisible attached property. In the next section I discuss how this data-binding allows the visibility of the pane to be programmatically controlled via the view-model.

View-model Control of Pane Visibility

Unfortunately AvalonDock provides no settable IsVisible property that we can data-bind to our view-model. To work around this I use the WPF attached property mechanism to externally create and attach a new property to an AvalonDock pane to achieve the desired functionality.

IsPaneVisible serves this purpose, let's look at that data-binding again:

    AvalonDockMVVM:AvalonDockHost.IsPaneVisible="{Binding IsVisible}"

IsPaneVisible is intended only to be attached to an AvalonDock DockableContent. It gives us a boolean property that can be set to true or false to either show or hide the pane. Of course it is not intended to be used directly, rather it has been data-bound to the view-model's IsVisible property. Henceforth we can use the view-model property to programmaticaly change the visibility of panes.

This image illustrates the IsPaneVisible binding that was shown in the previous XAML snippets:

In the implementation section I'll explain in more detail how IsPaneVisible works. For now it is enough to know that pane visibility can be manipulated through the view-model.

A good example can be seen in the XAML for the main window's View menu. The menu item's IsChecked is data-bound to the view-model's IsVisible:

    <MenuItem
        Header="_Open Documents"
        IsChecked="{Binding OpenDocumentsPaneViewModel.IsVisible}"
        IsCheckable="True"
        />

Clicking this menu item now toggles the visibility of the Open Documents pane and no extra commands or code-behind are required.

The ShowAllPanes method is an example of programatically setting IsVisible:

    /// <summary>
    /// Show all panes.
    /// </summary>
    public void ShowAllPanes()
    {
        foreach (var pane in this.Panes)
        {
            pane.IsVisible = true;
        }
    }

This method operates entirely in the view-model, iterating all panes and setting the IsVisible for each to true. HideAllPanes works in a similar way but sets the property to false.

Setting the Active Document or Active Pane

Let's refresh our memory of the data-bindings for ActiveDocument and ActivePane that we looked at earlier:

    <AvalonDockMVVM:AvalonDockHost
        ...
        ActiveDocument="{Binding ActiveDocument}"
        ActivePane="{Binding ActivePane}"
        ...
        />

The data-binding allows AvalonDock's currently selected document or pane to be queried and manipulated via the view-model.

There is an example of this in the code for the Open Documents pane. Selecting a document in the open documents list causes that document to be activated. OpenDocumentsPaneViewModel's ActiveDocument property forwards through to MainWindowViewModel's ActiveDocument:

    /// <summary>
    /// View-model for the active document.
    /// </summary>
    public TextFileDocumentViewModel ActiveDocument
    {
        get
        {
            return this.MainWindowViewModel.ActiveDocument;
        }
        set
        {
            this.MainWindowViewModel.ActiveDocument = value;
        }
    }

As MainWindowViewModel's ActiveDocument is data-bound to AvalonDockHost's ActiveDocument changes to the one are propagated to the other and causes the associated AvalonDock component to be activated. By extension then changes to OpenDocumentsPaneViewModel's ActiveDocument also set the active document.

Now looking at OpenDocumentsPaneView.xaml we see that OpenDocumentsPaneViewModel's ActiveDocument is data-bound as the ListBox's selected item:

    <!-- ListBox that displays the list of open documents. -->
    <ListBox
        x:Name="documentsListBox"
        Grid.Row="0"
        ItemsSource="{Binding Documents}"
        SelectedItem="{Binding ActiveDocument}"
        />

So whenever the user changes the selection in the list, that choice sets the active document in AvalonDock. The same applies in the reverse direction. When the user interacts directly with AvalonDock and selects a document that change is propagated through to the view-model and ultimately sets ListBox's selected item.

For the change to be propagated OpenDocumentsPaneViewModel handles MainWindowViewModel's ActiveDocumentChanged event. In response it raises its own PropertyChanged event for its own ActiveDocument property. This causes data-binding to update the ListBox's selected item:

    /// <summary>
    /// Event raised when the active document in the main window has changed.
    /// </summary>
    private void MainWindowViewModel_ActiveDocumentChanged(object sender, EventArgs e)
    {
        OnPropertyChanged("ActiveDocument");
    }

The discussion above also applies to setting the active pane, although instead using the ActivePane property.

AvalonDock Layout

One problem that I encountered while developing the code was how I should handle the default layout of documents and panes. Normally when using AvalonDock in a non-MVVM style you would create the default layout directly in MainWindow.xaml in the way that was described in the AvalonDock getting started tutorial. While the application is running the user is able to rearrange the layout to their own tastes and AvalonDock conveniently provides save and restore layout methods that make it remarkably easy to persist user-layout between sessions.

However when using AvalonDock with MVVM you will find that it is not possible to directly specify the default layout in the XAML. This is because it is AvalonDockHost, my AvalonDock wrapper, that provides the default layout that you would normally hard-code in MainWindow.xaml. AvalonDockHost's default layout is simple and can't be customized to your needs. I could have added features to AvalonDockHost that would have allowed customization of its default layout, although I think at best this would have been a clumsy solution. Fortunately I came up with a better solution that is simple to use and was easy to implement.

In short, you must create a file that contains the default layout, make this file a part of the application as an embedded resource, and then when your application is loaded the first time it should restore its layout from this embedded default layout file.

To generate the layout file in the first place your application should be up and running with AvalonDock. After rearranging the panes into a satisfying default layout you should save the layout file using AvalonDock's built-in SaveLayout method. This will be easy if you already have your application setup to save user-layout when the application closes. For example, in the sample application, SaveLayout is called when the application exits:

    /// <summary>
    /// Event raised when the window is about to close.
    /// </summary>
    private void Window_Closing(object sender, CancelEventArgs e)
    {
        ...

        //
        // When the window is closing, save AvalonDock layout to a file.
        //
        avalonDockHost.DockingManager.SaveLayout(LayoutFileName);
    }

The generated layout file should now be added to the application as an Embedded Resource. If the file type is set to something else you won't be able to retreive the resource, so beware.

This screenshot shows the default layout file as an embedded resource in the sample application's project:

This screenshot shows the properties for default layout file. Note that Build Action is set to Embedded Resource:

Now the final piece of the default layout puzzle. The default layout should be applied when there is no custom user-layout file existing on disk. This will be the case obviously when the application is run for the first time but it also allows the user to delete their custom layout file so that the application's layout reverts to the default next time it is restarted.

Layout, whether embedded default layout or custom user-layout, can only be restored once AvalonDock has loaded. To this end AvalonDockHost raises the AvalonDockLoaded event which is handled by the sample application:

    <AvalonDockMVVM:AvalonDockHost
        ...
        AvalonDockLoaded="avalonDockHost_AvalonDockLoaded"
        ...
        />

The event-handler either loads a custom user-layout file or it loads the embedded default layout file. In the first case, if a custom user-layout file already exists, we simply defer to AvalonDock's RestoreLayout method to load the file and restore the user's layout:

    /// <summary>
    /// Event raised when AvalonDock has loaded.
    /// </summary>
    private void avalonDockHost_AvalonDockLoaded(object sender, EventArgs e)
    {
        if (System.IO.File.Exists(LayoutFileName))
        {
            //
            // If there is already a saved layout file, restore AvalonDock layout from it.
            //
            avalonDockHost.DockingManager.RestoreLayout(LayoutFileName);
        }
        else
        {
            //
            // ... no previously saved layout exists, need to load default layout ...
            //
        }
    }

In the second case, where there is no existing user-layout file, the layout is restored using a stream that reads the embedded resource:

    /// <summary>
    /// Event raised when AvalonDock has loaded.
    /// </summary>
    private void avalonDockHost_AvalonDockLoaded(object sender, EventArgs e)
    {
        if (System.IO.File.Exists(LayoutFileName))
        {
            //
            // ... load existing custom user-layout file ...
            //
        }
        else
        {
            //
            // Load the default AvalonDock layout from an embedded resource.
            //
            var assembly = Assembly.GetExecutingAssembly();
            using (var stream = assembly.GetManifestResourceStream(DefaultLayoutResourceName))
            {
                avalonDockHost.DockingManager.RestoreLayout(stream);
            }
        }
    }

The embedded resource is retrieved by specifying the full resource name, which in this case is:

    /// <summary>
    /// Name of the embedded resource that contains the default AvalonDock layout.
    /// </summary>
    private static readonly string DefaultLayoutResourceName = "SampleApp.Resources.DefaultLayoutFile.xml";

The obvious question now is how did I figure out what name to use?

From my example you could probably infer the name of any embedded resource. It appears to be a combination of <namespace> + <resource-path> + <resource-file-name>, or something like that. However there is a fool proof way to know for sure.

The following code retrieves an array of the full names of all embedded resources:

    string[] names = this.GetType().Assembly.GetManifestResourceNames();

You can print this list to debug output and then pick out the name of the resource.

User Confirmation for Closing Files

When the user attempts to close a file that has been modified, but not saved, a dialog box is invoked to request confirmation of whether the file should really be closed. This also happens when the user closes all files or exits the application when any of the open files have been modified.

The code for this is not specifically related to AvalonDock, so I won't cover it in great detail, but it is a good chance for me to explain various aspects of the sample application.

The key to the feature is TextFileDocumentViewModel's IsModified property. You may remember from the earlier XAML snippet that a data-binding is setup in such a way that any changes the user makes in the TextBox are immediately propagated to the view-model's Text property. It is the setter for the Text property that sets IsModified to true. In this way, whenever the user changes the text, the document is marked as modified. The subsequent PropertyChanged event fired for IsModified causes a cascade of action that result in the update of the document's title and tooltip and the application's titlebar.

The CloseFile method in the view-model doesn't use IsModified directly, instead it calls the helper method QueryCanCloseFile (which we also saw earlier) to check whether it can close the file:

    /// <summary>
    /// Close the specified document.
    /// Returns 'true' if the user allowed the file to be closed,
    /// or 'false' if the user canceled closing of the file.
    /// </summary>
    public bool CloseFile(TextFileDocumentViewModel document)
    {
        if (!QueryCanCloseFile(document))
        {
            //
            // User has chosen not to close the file.
            //
            return false;
        }

        this.Documents.Remove(document);

        //
        // File has been closed.
        //
        return true;
    }

A modified document is only actually closed (i.e. removed from the Documents collection) provided the user has confirmed the action.

QueryCanClose invokes the confirmation dialog for modified documents:

    /// <summary>
    /// Determine if the file can be closed.
    /// If the file is modified, but not saved, the user is asked
    /// to confirm that the document should be closed.
    /// </summary>
    public bool QueryCanCloseFile(TextFileDocumentViewModel document)
    {
        if (document.IsModified)
        {
            //
            // Ask the user to confirm closing a modified document.
            //
            if (!this.DialogProvider.QueryCloseModifiedDocument(document))
            {
                // User doesn't want to close it.
                return false;
            }
        }

        return true;
    }

The confirmation dialog is invoked indirectly through the IDialogProvider interface and this keeps the view-model nicely separated from the view.

The close all files feature simply calls the CloseFile method for each open document and so it effectively operates in the same way.

Exiting the application, which implicitly closes all open documents, works in a similar way. The main window's Closing event calls OnApplicationClosing in the view-model. If any documents are modified it invokes a different dialog to request user confirmation:

    /// <summary>
    /// Called when the application is closing.
    /// Return 'true' to allow application to exit.
    /// </summary>
    public bool OnApplicationClosing()
    {
        if (this.AnyDocumentIsModified)
        {
            if (!this.DialogProvider.QueryCloseApplicationWhenDocumentsModified())
            {
                //
                // User has cancelled application exit.
                //
                return false;
            }
        }

        //
        // Allow application exit to proceed.
        //
        return true;
    }

Again the confirmation is indirectly invoked through IDialogProvider.

This concludes the walkthrough of the sample application. In the next part we will delve into the internals of AvalonDockHost.

Implementation

This part of the article is dedicated to understanding the implementation of the AvalonDockHost class. You can find AvalonDockHost in the AvalonDockMVVM project.

AvalonDockHost XAML

AvalonDockHost is a user control. Its declaration in AvalonDockHost.xaml is trivial:

    <UserControl 
        x:Class="AvalonDockMVVM.AvalonDockHost"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:AvalonDockMVVM"
        xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
        >
        <ad:DockingManager 
            x:Name="dockingManager"
            Loaded="AvalonDock_Loaded"
            />
    </UserControl>

The XAML declaration contains only the DockingManager, the root element in AvalonDock's visual-tree. It is named dockingManager and is directly referenced from the code-behind to add and remove AvalonDock components.

The Loaded event is handled and propagated to the application as the AvalonDockLoaded event. We have already seen how this event is handled in the sample application so that AvalonDock layout can be restored.

Synchronizing ActiveDocument and ActivePane

AvalonDock's ActiveContentChanged event is handled so that AvalonDockHost is notified when the active, or focused, panel has been changed.

The event is hooked in the constructor:

    public AvalonDockHost()
    {
        InitializeComponent();

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

        UpdateActiveContent();
    }

The call to UpdateActiveContent ensures that the ActiveDocument and ActivePane properties are set to correct initial values. UpdateActiveContent is also called by the ActiveContentChanged event-handler so that over time ActiveDocument and ActivePane remain synchronized with AvalonDock's internal state.

UpdateActiveContent queries AvalonDock for the currently active component and then updates either ActiveDocument or ActivePane depending on the component's type:

    /// <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;
            }
        }
    }

The application can set the ActiveDocument and ActivePane properties either programmatically or via data-binding and we have seen an example of this in the walkthrough. Internally both documents and panes are simply treated as panels so only a a single method handles the property changed event for both:

    /// <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();
        }
    }

contentMap is a dictionary that maps a panel's view-model to its associated AvalonDock component. The above snippet retreives the panel's AvalonDock component from contentMap and calls the Activate method on it. We will soon see how contentMap is initialized.

Synchronizing Documents and Panes

The Documents and Panes dependency properties are collections that provide the view-models for panels that AvalonDockHost transforms into AvalonDock components. Again we will see that both documents and panes are treated internally as panels.

There is a single property changed event-handler that is invoked when either of these properties have changed, for example when a new collection has been assigned, the contents of the collection are transformed into AvalonDock components and added to the DockingManager. The CollectionChanged event is hooked for the two collections so that AvalonDockHost is notified of any future changes, such as adding or removing documents and panes.

Before dealing with the newly assigned collection the event-handler deals with, if there was one, the previously assigned collection.

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

The CollectionChanged event makes AvalonDockHost aware of added or removed panels. In response to this event AvalonDock components are either instantated and added to AvalonDock or they are removed from AvalonDock.

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

ResetDocumentsOrPanes handles the Reset action and is delegated to a separate method because it is somewhat complicated. I have to admit that it is a bit of a hack. Mostly I have been able to use the same event-handlers and methods to deal with both documents and panes, or panels as I collectively call them. In this case though I can't and I'll blame this on what I perceive as an inadequacy in CollectionChanged's Reset action. The problem is that the Reset action does not supply a list of the items that were removed. Instead, it appears, you are expected to keep a separate internal list of the items so that you already know the items that were previously in the collection. This is an annoying problem and in the past (eg my previous article ) I solved it by creating my own version of ObservableCollection that works in a friendlier way. In this article though I was aiming to minimize dependencies so I haven't introduced that class.

Given that documents and panes are treated as panels internally and that ResetDocumentsOrPanes should remove either all documents or all panes it must figure out which type of object to remove. To acheive this it compares the event sender to the Documents and Panes properties to determine which of the two collections has raised the CollectionChanged event. After this test it knows what type of panel it should remove and proceeds to remove them by inspecting the only internal collection that AvalonDockHost does have, the contentMap dictionary (something we will look at soon). This is not my best ever piece of code but it does the job and is a convenient solution to an annoying problem. Please examine the method yourself if you want (and please message me if you think of a less hacky, but still just as convenient, solution).

Moving on, the AddPanels method calls AddPanel for each of the new panels. AddPanel then is where the interesting stuff happens: it is here that a panel view-model is transformed into an AvalonDock component. And as we have seen in the walkthrough the AvalonDock component will be specified by a data-template. The visual-tree must be searched to find the data-template resource. If a data-template is found that corresponds to the type of the view-model it is instantiated and the resulting UI element, expected to be an AvalonDock component, is added to AvalonDock.

Searching the visual-tree for a data-template is something that WPF already does internally but unfortunately it does not appear to provide programatic access to this feature. So I created my own method, DataTemplateUtils.InstanceTemplate, that searches for and instantiates the data-template. I won't cover the internals of this method, but please feel free to inspect the code yourself.

So with all that in mind AddPanel's first task is to find and instantiate the AvalonDock component:

    /// <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);
        }
        
        // ... rest of method ...
    }

Next the instantiated UI element must be checked to ensure that it is actually an AvalonDock component. For this it is is type-cast to an AvalonDock ManagedContent, the base-class for both AvalonDock documents and panes. If the instantiated UI element is not a ManagedContent it therefore is not an AvalonDock component and an exception is thrown. After verifying the AvalonDock component it is added to the contentMap dictionary so that it can be easily retrieved later:

    private void AddPanel(object panel)
    {
        // ... instantiate uiElement from data type of view-model ...

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

        // ... rest of method ...
    }

Next various events are hooked to keep track of the AvalonDock component's ongoing state:

    private void AddPanel(object panel)
    {
        // ... instantiate uiElement from data type of view-model ...
        
        // ... type-cast to ManagedContent and add entry to contentMap ...

        //
        // 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."); 
            }
        }
        
        // ... rest of method ...
    }

The Closed event is handled for every AvalonDock component and ensures that any panel that is closed is also internally removed from AvalonDockHost.

Another event is hooked depending on the type of the panel. For documents it is the Closing event and handling it allows AvalonDockHost to be notified when a document is being closed after the user has clicked the AvalonDock document close button. In this situation AvalonDockHost raises the DocumentClosing event so that the application is aware that the document is closing.

For panes the StateChanged event is hooked. AvalonDockHost handles this event so that it is notified when pane visibility has changed. In response it sets the IsPaneVisible attached property to true or false to indicate the pane's visibility.

Continuing on, AddPanel's last task is to show and active the AvalonDock component:

    private void AddPanel(object panel)
    {
        // ... instantiate uiElement from data type of view-model ...
        
        // ... type-cast to ManagedContent and add entry to contentMap ...
        
        // ... hook events on the ManagedContent ...

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

Now let's look at RemovePanel. This method is called when either a document or pane has been removed from the Documents or Panes collections. The AvalonDock component is retrieved from contentMap and closed:

    /// <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;
            }
        }
    }

When a document is being closed the Closing event is raised and the disableClosingEvent variable that was set to true in RemovePanel comes into play here to prevent the DocumentClosingEvent being propagated to the application:

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

As RemovePanel is only called when the application itself has removed a panel from the Documents or Panes collection the DocumentClosing event doesn't need to be raised because the application is already aware that the document is closing. When it is a document (and not a pane) that is removed the Closing event is handled by AvalonDockHost. As it will have been the application that removed the document it is thus already aware that the document has been closed and there is no need to raise the DocumentClosing event, hence the use of disableClosingEvent to prevent raising of the DocumentClosing event.

When a user has clicked the AvalonDock document close button it is AvalonDock itself (and not the application or AvalonDockHost) that calls the ManagedContent's Close method. This also causes the Closing event to be raised and in this case disableClosingEvent will be set to its default value, which is false. This all means that in this case that DocumentClosing is raised to notify the application. This is what we want because the application has no other way of knowing that a document is about to be closed and it is important that the application, and hence the user, is given the chance to veto closing of the document.

Ultimately after a panel has been closed its Closed event is raised. In response to this event AvalonDockHost removes the panel's contentMap entry, unhooks events and performs other cleanup tasks:

    /// <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;
                }
            }
        }
    }

The Closed event-handler also ensures that the document or pane has been removed from the Documents or Panes collection. This is important for when a document is closed by the AvalonDock document close button, for this is the only way that the document is actually removed from the view-model. If it happens to be the active document or pane that has ben closed either ActiveDocument or ActivePane are reset to null.

IsPaneVisible Attached Property

In this section I discuss the implementation of the IsPaneVisible attached property. This property is attached to an AvalonDock pane to allow its visibility to be controlled by a boolean variable. The main intention is to data-bind the property to a view-model property and thus allow pane visibility to be controlled entirely within the view-model.

We have already seen earlier how the StateChanged is hooked by AvalonDockHost. This event is raised when the visibility of a pane has changed, for example when a pane is closed after the user has clicked the AvalonDock pane hide button.

The event-handler sets the attached property's value to true or false based on the pane's current visibility:

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

In the walkthrough we looked at the data-templates for the sample application's panes in MainWindow.xaml. Let's review the Open Documents pane data-template to refresh our memory:

    <DataTemplate
        DataType="{x:Type ViewModels:OpenDocumentsPaneViewModel}"
        >
        <ad:DockableContent
            x:Name="openDocumentsPane"
            Title="Open Documents"
            AvalonDockMVVM:AvalonDockHost.IsPaneVisible="{Binding IsVisible}"
            >
            <local:OpenDocumentsPaneView />
        </ad:DockableContent>
    </DataTemplate>

The data-binding references IsPaneVisible as AvalonDockMVVM:AvalonDockHost.IsPaneVisible. This is the standard syntax for referencing an attached property in a data-binding. In this example IsPaneVisible is data-bound to OpenDocumentPaneViewModel's IsVisible property. With this data-binding in place, and as we have already seen in the walkthrough, the visibility of the pane can be controlled via the view-model.

IsPaneVisible is registered as an attached property by AvalonDockHost:

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

It is the property changed event-handler that does the main work of the attached property:

    /// <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();
            }
        }
    }

When the IsPaneVisible property is changed the event-handler is invoked and the pane is either shown or hidden depending on IsPaneVisible's new value.

This brings us to the end of the AvalonDockHost implementation section. Next is the conclusion followed up by the AvalonDockHost reference section.

Conclusion

This article has discussed the use and implementation of AvalonDockHost. AvalonDockHost is a an AvalonDock wrapper I have created to adapt AvalonDock for use in an MVVM application.

Thanks for taking the time to read the article.

Reference

This section is a reference for the properties of AvalonDockHost.

Panes

Gets and sets the collection of view-model objects for panes.

Adding an object to this collection results in a pane being added to AvalonDock.

A DataTemplate should be defined for each type of pane and the root of the DataTemplate should be 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.

Documents

Gets and sets the collection of view-model objects for documents.

Adding an object to this collection results in a document being added to AvalonDock.

A DataTemplate should be defined for each type of document and the root of the DataTemplate should be an AvalonDock DocumentContent.

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.

ActivePane

Gets and sets the view-model object for the currently active pane.

Setting this property programatically changes the active focused AvalonDock panel.

It can also be data-bound to a view-model property so that the active pane can be set via the view-model. This property is automatically updated when the user directly selects an AvalonDock pane.

ActiveDocument

Gets and sets the view-model object for the currently active document.

Setting this property programatically changes the active focused AvalonDock panel.

It can also be data-bound to a view-model property so that the active document can be set via the view-model.

This property is automatically updated when the user directly selects an AvalonDock document.

IsPaneVisible

Gets or sets the visibility state of a pane.

This attached property is intended to only be attached to an AvalonDock DockableContent that is the root element in the DataTemplate for a pane's view-model.

Setting this property to true shows the pane, setting to false hides the pane. This property is automatically updated when the user directly hides an AvalonDock pane by clicking the AvalonDock pane hide button.

DockingManager

Gets the AvalonDock DockingManager.

Direct access to the DockingManager allows the application to save and restore AvalonDock layout.

AvalonDockLoaded

This event is raised when AvalonDock has been loaded.

The application can respond to this event by restoring AvalonDock layout.

DocumentClosing

This event is raised when a document is being closed after the user has clicked the AvalonDock document close button. It allows the application to cancel, if necessary, the document close operation.

NOTE: This event is not raised when a document is closed by the application removing the document from the Documents collection itself. In this case the application is already aware that the document is being closed and the event is not needed.

SetIsPaneVisible

Sets the value of the IsPaneVisible attached property.

Although it should not be necessary to use this directly as IsPaneVisible is intended to be data-bound to a view-model property and the view-model should be set instead of calling this method.

GetIsPaneVisible

Gets the value of IsPaneVisible.

It should not be necessary to use this directly for the reason described above.

Update History

  • 11/08/2011: Article first published.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here