Exception on RestoreLayout

Jan 18, 2009 at 8:14 AM
Edited Jan 18, 2009 at 8:16 AM
The RestoreLayout function seems to fail for certain transitions. Here is how to reproduce the crash with the AvalonDockTest project.

Change the demo.xaml file to only have the DocumentPane "_documentHost" and the DockableContent "_objectExplorerHost" in a single ResizingPanel

            <ad:DockingManager x:Name="_dockingManager">
                <ad:ResizingPanel Orientation="Horizontal">
                    <ad:DocumentPane x:Name="_documentsHost">
                        <ad:DocumentContent Title="Home" InfoTip="C:\Program files\Italy\Converters\AvalanDock\AvalonDock.xps  Here your infos..etc.." ContentTypeDescription="XPS document">
                            <FlowDocumentScrollViewer>
                                <FlowDocument FontFamily="Segoue">
                                    <Paragraph>
                                        <Hyperlink x:Name="SaveLayout" Click="SaveLayout_Click">Save layout</Hyperlink>
                                        <Hyperlink x:Name="RestoreLayout"  Click="RestoreLayout_Click">Restore layout</Hyperlink>
                                    </Paragraph>
                                </FlowDocument>
                            </FlowDocumentScrollViewer>
                        </ad:DocumentContent>
                    </ad:DocumentPane>
                    <ad:DockablePane ad:ResizingPanel.ResizeWidth="200">
                        <ad:DockableContent x:Name="_objectExplorerHost" Title="Object explorer" >
                            <ad:DockableContent.Icon>
                                <Image Width="16" Height="16" Source="Images\date.png"/>
                            </ad:DockableContent.Icon>
                        </ad:DockableContent>
                    </ad:DockablePane>
                </ad:ResizingPanel>
            </ad:DockingManager>

Now comment out references to the windows we removed ("_propertiesWindow", "_explorerWindow", "_eventsLogWindow", etc).

Run the application and click "Save Layout" to save the object window in the docked position. Now drag the object window outside the main window so it is a floating window. Then click "Restore Layout" and your will get the exception. It tries to pass a null element to InsertChildRelativeTo from inside Anchor.

Anyone have a fix for this? I'll continue to try and get a grasp of the intended logic and see if I can work out where the bug is...
Jan 23, 2009 at 4:33 AM
Edited Jan 30, 2009 at 8:51 PM
I ended up rerwriting the "RestoreLayout" functionality after encountering a few other crashes/issues related to odd xml layouts. It also wasn't restoring panel sizes properly all the time. If anyone is interested, below is my fixed up interface named "RJ_RestoreLayout" (I made it function alongside the old RestoreLayout). Just add the sections of code below and call RJ_ResoreLayout instead of RestoreLayout to try it.



Update DockableFloatingWindow::OnClosed to the following (adds an if statement)
protected override void OnClosed(EventArgs e)
{
    base.OnClosed(e);

    if( HostedPane != null )
    {
        while (HostedPane.Items.Count > 0)
        {
            DockableContent dockableContent = HostedPane.Items[0] as DockableContent;
            Manager.Hide(dockableContent);
        }
    }

    Manager.UnregisterFloatingWindow(this);
}


Put this inside class DockableContent
public virtual void RJ_RestoreLayout(XmlNode contentNode) {  }


Put this inside class DockingManger
#region RJ Restore Layout

/// RJ_RestoreDocumentPaneLayout
/// This function loads the DockableContent elements specified in XML into the given document pane
void RJ_RestoreDocumentPaneLayout
(
    XmlNode         documentPaneNode,   // xml element describing the document pane
    DocumentPane    documentPane        // document pane to load into
)
{
    Debug.Assert( documentPane != null );
    
    foreach (XmlNode contentNode in documentPaneNode.ChildNodes)
    {
        XmlAttribute nameAttr = contentNode.Attributes["Name"];
        if (nameAttr != null)
        {
            // only search contents that haven't already been loaded
            foreach (DockableContent content in _hiddenContents)
            {
                Debug.Assert( content.State == DockableContentState.Hidden );
                if (content.Name == nameAttr.Value)
                {
                    // remove from hidden list
                    _hiddenContents.Remove(content);

                    // insert and set state
                    documentPane.Items.Add(content);
                    content.SetStateToDocument();
                        
                    // call custom persistance code
                    content.RJ_RestoreLayout(contentNode);
                    break;
                }
            }
        }
    }
}

/// RJ_RestoreLayout
/// This function returns a resizing panel loaded from a given XML element
ResizingPanel RJ_RestoreLayout
(
    XmlNode                     resizingPanelNode,    // xml element describing the resizing panel
    DocumentPane                mainDocumentPane,    // main document pane
    DocumentPaneResizingPanel   mainDocumentPanel    // panel containing the main document pane
)
{
    // create the new panel
    ResizingPanel resizingPanel = new ResizingPanel();

    // parse the orientation
    XmlAttribute orientationAttr = resizingPanelNode.Attributes["Orientation"];
    if (orientationAttr != null)
        resizingPanel.Orientation = (Orientation)Enum.Parse(typeof(Orientation), orientationAttr.Value);

    // load the children
    foreach (XmlNode childNode in resizingPanelNode.ChildNodes)
    {
        if (childNode.Name == "ResizingPanel")
        {
            // recursively load the child panel
            ResizingPanel childResizingPanel = RJ_RestoreLayout(childNode, mainDocumentPane, mainDocumentPanel);

            // parse the dimension attributes
            XmlAttribute resizingWidthAttr = childNode.Attributes["ResizeWidth"];
            if (resizingWidthAttr != null )
                ResizingPanel.SetResizeWidth(childResizingPanel, XmlConvert.ToDouble(resizingWidthAttr.Value));

            XmlAttribute resizingHeightAttr = childNode.Attributes["ResizeHeight"];
            if (resizingHeightAttr != null)
                ResizingPanel.SetResizeHeight(childResizingPanel, XmlConvert.ToDouble(resizingHeightAttr.Value));

            // add the new child
            resizingPanel.Children.Add(childResizingPanel);
        }
        #region Restore DockablePane
        else if (childNode.Name == "DockablePane")
        {
            // create the new pane
            DockablePane dockablePane = new DockablePane();

            // parse the anchor attribute
            XmlAttribute anchorAttr = childNode.Attributes["Anchor"];
            if (anchorAttr != null)
                dockablePane.Anchor = (AnchorStyle)Enum.Parse(typeof(AnchorStyle), anchorAttr.Value);

            // parse the dimension attributes
            XmlAttribute resizingWidthAttr = childNode.Attributes["ResizeWidth"];
            if (resizingWidthAttr != null )
                ResizingPanel.SetResizeWidth(dockablePane, XmlConvert.ToDouble(resizingWidthAttr.Value));

            XmlAttribute resizingHeightAttr = childNode.Attributes["ResizeHeight"];
            if (resizingHeightAttr != null)
                ResizingPanel.SetResizeHeight(dockablePane, XmlConvert.ToDouble(resizingHeightAttr.Value));

            // create the child content elements while keeping track of a need to enable autohide
            bool toggleAutoHide = false;
            foreach (XmlNode contentNode in childNode.ChildNodes)
            {
                XmlAttribute nameAttr = contentNode.Attributes["Name"];
                if (nameAttr != null)
                {
                    // only search contents that haven't already been loaded
                    foreach (DockableContent content in _hiddenContents)
                    {
                        Debug.Assert( content.State == DockableContentState.Hidden );
                        if (content.Name == nameAttr.Value)
                        {
                            // remove from the hidden list
                            _hiddenContents.Remove(content);

                            // insert and set state
                            dockablePane.Items.Add(content);
                            content.SetStateToDock();
                        
                            // check if we need to enable autohide
                            XmlAttribute autoHideAttr = contentNode.Attributes["AutoHide"];
                            if(    autoHideAttr != null && XmlConvert.ToBoolean(autoHideAttr.Value) && dockablePane.Items.Count == 1 )
                            {
                                toggleAutoHide = true;
                            }

                            // call custom persistence code
                            content.RJ_RestoreLayout(contentNode);

                            break;
                        }
                    }
                }
            }

            // if our new pane contains any items
            if (dockablePane.Items.Count > 0)
            {
                // enable auto hide
                if (toggleAutoHide)
                    ToggleAutoHide(dockablePane);

                // add pane to the resizing panel
                // TODO: See if we really need this 'if' statement. (i.e. Could ToggleAutoHide
                //       have changed the number of pane items?)
                if (dockablePane.Items.Count > 0)
                    resizingPanel.Children.Add(dockablePane);
            }
        } 
        #endregion
        #region Restore Contents inside a DocumentPane
        else if (childNode.Name == "DocumentPanePlaceHolder")
        {
            // we can only load in the main document pane if it hasn't been loaded yet
            if(        (mainDocumentPanel != null && mainDocumentPanel.Parent == null)
                ||    (mainDocumentPanel == null && mainDocumentPane.Parent == null) )
            {
                RJ_RestoreDocumentPaneLayout(childNode, mainDocumentPane);

                // attach the document panel as the root content (use the pane if no panel exists)
                if (mainDocumentPanel != null)
                    resizingPanel.Children.Add(mainDocumentPanel);
                else
                    resizingPanel.Children.Add(mainDocumentPane);
            }
        }
        
        #endregion
    }

    // return our loaded panel
    return resizingPanel;
}


public void RJ_RestoreLayout(Stream backendStream)
{
    XmlDocument doc = new XmlDocument();

    doc.Load(backendStream);

    RJ_RestoreLayout(doc);
}

public void RJ_RestoreLayout(XmlReader reader)
{
    XmlDocument doc = new XmlDocument();

    doc.Load(reader);

    RJ_RestoreLayout(doc);
}

public void RJ_RestoreLayout(string filename)
{
    XmlDocument doc = new XmlDocument();

    doc.Load(filename);

    RJ_RestoreLayout(doc);
}

public void RJ_RestoreLayout(TextReader reader)
{
    XmlDocument doc = new XmlDocument();

    doc.Load(reader);

    RJ_RestoreLayout(doc);
}

/// HideAllDockableContent
/// This function loads a new layout from XML
void HideAllDockableContent()
{
    DockableContent[] dockableContents = DockableContents;
    foreach (DockableContent content in dockableContents)
    {
        Hide(content);
    }
}

/// DetachMainDocumentPane
/// Extract the main document pane and it's panel. This will detac the panel from it's
/// parent. If there is no panel, it will detach the document pane from its parent. Null
/// values are out put if nothing is found.
void DetachMainDocumentPane
(
    out DocumentPane                mainDocumentPane,
    out DocumentPaneResizingPanel   mainDocumentPanel
)
{
    // Extract the main document pane and it's panel
    mainDocumentPane = (Content is DocumentPane) ? Content as DocumentPane : GetMainDocumentPane(Content as ResizingPanel);
    if( mainDocumentPane == null )
    {
        mainDocumentPanel = null;
        return;
    }

    // extract the panel from the main document pane
    mainDocumentPanel = mainDocumentPane.GetParentDocumentPaneResizingPanel();

    // detach the main document panel from its parent so that it can be reinserted at the appropriate position
    if (mainDocumentPanel != null)
    {
        if (mainDocumentPanel.Parent is ResizingPanel)
            ((ResizingPanel)mainDocumentPanel.Parent).RemoveChild(mainDocumentPanel);
        else if (mainDocumentPanel.Parent is DockingManager)
            ((DockingManager)mainDocumentPanel.Parent).Content = null;
    }
    else
    {
        // there was no main document panel, so detach the main document pane instead
        if (mainDocumentPane.Parent is ResizingPanel)
            ((ResizingPanel)mainDocumentPane.Parent).RemoveChild(mainDocumentPane);
        else if (mainDocumentPane.Parent is DockingManager)
            ((DockingManager)mainDocumentPane.Parent).Content = null;
    }
}


/// RJ_RestoreLayout
/// This function loads a new layout from XML
void RJ_RestoreLayout(XmlDocument doc)
{
    // Hide all dockable contents so that any contents which do not exists in the loaded layout will be left
    // in the hidden state rather than in a display state but outside the loaded content tree
    HideAllDockableContent();

    // Extract the main document pane and it's panel
    DocumentPane mainDocumentPane;
    DocumentPaneResizingPanel mainDocumentPanel;
    DetachMainDocumentPane(out mainDocumentPane, out mainDocumentPanel);
    if( mainDocumentPane == null )
    {
        //TODO: Investigate supporting a layout with no document pane
        throw new Exception("Failed to find an existing document pane");
    }

    // Clear the current Content
    this.Content = null;

    // Load the XML
    try
    {
        // validate root node name format
        if (doc.DocumentElement == null || doc.DocumentElement.Name != "DockingManager")
        {
            throw new Exception("Failed to find DockingManager root element");
        }

        // find the potential root child nodes
        XmlNode resizingPanelNode    = doc.DocumentElement.SelectSingleNode("ResizingPanel");
        XmlNode documentPaneNode    = doc.DocumentElement.SelectSingleNode("DocumentPanePlaceHolder");
        XmlNode windowsNode            = doc.DocumentElement.SelectSingleNode("Windows");

        // Load the root content
        if (resizingPanelNode != null)
        {
            this.Content = RJ_RestoreLayout(resizingPanelNode, mainDocumentPane, mainDocumentPanel);
        }
        else if (documentPaneNode != null)
        {
            // the main document pane shouldn't have a parent at this point
            Debug.Assert(        (mainDocumentPanel != null && mainDocumentPanel.Parent == null)
                            ||    (mainDocumentPanel == null && mainDocumentPane.Parent == null) );

            // the document pane was at the root, so load the data into it
            RJ_RestoreDocumentPaneLayout(documentPaneNode, mainDocumentPane);

            // attach the document panel as the root content (use the pane if no panel exists)
            if (mainDocumentPanel != null)
                this.Content = mainDocumentPanel;
            else
                this.Content = mainDocumentPane;
        }
        else
        {
            throw new Exception("Failed to find root content element in XML.");
        }

        //restore floating windows
        foreach (XmlNode floatingWindowNode in windowsNode.ChildNodes)
        {
            // parse the attributes
            bool isDockableWindow    = XmlConvert.ToBoolean( floatingWindowNode.Attributes["IsDockableWindow"].Value );
            Point location            = new Point(    XmlConvert.ToDouble(floatingWindowNode.Attributes["Left"].Value),
                                                    XmlConvert.ToDouble(floatingWindowNode.Attributes["Top"].Value) );
            Size size                = new Size(    XmlConvert.ToDouble(floatingWindowNode.Attributes["Width"].Value),
                                                XmlConvert.ToDouble(floatingWindowNode.Attributes["Height"].Value) );

            // create the new floating window
            DockableFloatingWindow floatingWindow = new DockableFloatingWindow(this);
            floatingWindow.Left        = location.X;
            floatingWindow.Top        = location.Y;
            floatingWindow.Width    = size.Width;
            floatingWindow.Height    = size.Height;
            floatingWindow.Owner    = Window.GetWindow(this);

            XmlNode dockablePaneNode = floatingWindowNode.SelectSingleNode("DockablePane");
            if( dockablePaneNode != null )
            {
                // create the new floating dockable pane
                FloatingDockablePane floatingDockablePane = new FloatingDockablePane(floatingWindow);

                // parse the dimension attributes
                XmlAttribute resizingWidthAttr = dockablePaneNode.Attributes["ResizeWidth"];
                if (resizingWidthAttr != null )
                    ResizingPanel.SetResizeWidth(floatingDockablePane, XmlConvert.ToDouble(resizingWidthAttr.Value));

                XmlAttribute resizingHeightAttr = dockablePaneNode.Attributes["ResizeHeight"];
                if (resizingHeightAttr != null)
                    ResizingPanel.SetResizeHeight(floatingDockablePane, XmlConvert.ToDouble(resizingHeightAttr.Value));
    
                // parse the anchor attribute
                floatingDockablePane.Anchor = (AnchorStyle)Enum.Parse(typeof(AnchorStyle), dockablePaneNode.Attributes["Anchor"].Value);

                // parse the content elements                        
                foreach (XmlNode contentNode in dockablePaneNode.ChildNodes)
                {
                    // only search contents that haven't already been loaded
                    foreach (DockableContent content in _hiddenContents)
                    {
                        Debug.Assert( content.State == DockableContentState.Hidden );
                        if (contentNode.Attributes["Name"].Value == content.Name)
                        {
                            // remove from the hidden list
                            _hiddenContents.Remove(content);

                            // insert and set state
                            floatingDockablePane.Items.Add(content);
                            content.SetStateToDock();

                            // call custom persistence code
                            content.RJ_RestoreLayout(contentNode);
                            break;
                        }
                    } 
                }

                // if we successfully added any content, register and finish creating the window
                if( floatingDockablePane.Items.Count > 0 )
                {
                    floatingWindow.HostedPane = floatingDockablePane;
                    floatingWindow.IsDockableWindow = isDockableWindow;

                    RegisterFloatingWindow(floatingWindow);

                    floatingWindow.ApplyTemplate();
                    floatingWindow.Show();
                }
                else
                {
                    // dispose of the empty window
                    floatingWindow.Close();
                }
            }

        }
    }
    catch( Exception e )
    {
        // rehide all content
        HideAllDockableContent();
        
        // attach the document panel as the root content (use the pane if no panel exists)
        if (mainDocumentPanel != null)
            this.Content = mainDocumentPanel;
        else
            this.Content = mainDocumentPane;

        // forward the exception
        throw e; 
    }
}

#endregion
Jan 25, 2009 at 10:33 PM
I updated the code above to fix an issue with a saved layout that contained a floating window with a dockable content that did not exist when the layout was restored. I made it close the empty floating window (rather than let it lie around unshown for the duration of the application). I also made it safe to cloase a floating window without a HostedPane by modifying DockableFloatingWindow::OnClosed
Jan 30, 2009 at 4:59 PM
Edited Jan 30, 2009 at 5:02 PM
Jucket

I tried updating the DockableFloatingWindow::OnClosed handler but the dockableContent.Closable property and the DockingManager.Close() method are not recognized? Do you have a different version of the source code or have you made more extensive changes than you have posted?

Thanks for posting up your changes. I have made a few fixes and have to build some feature content but wish I was working from the most recent source. (I posted about the empty SVN repository but no responses.) I plan on posting them after some testing time but its difficult to post lots of small changes.

Thanks again.
Bryan

Jan 30, 2009 at 8:53 PM
Edited Jan 30, 2009 at 8:54 PM
bryanroth:

Oops.  I have some other local changes to my AvalonDock stuff that lets you make the DockableContent closable in addition to hidable. I removed that line from the code above so that it should all work with the source code that is available. Hope that works for ya.