Toolwindow lost after being saved

Docking/MDI for WPF Forum

Posted 8 months ago by Michael Janulaitis - Corner Bowl Software
Version: 24.1.2
Platform: .NET 3.0
Environment: Windows 11 (64-bit)
Avatar

I have a very difficult bug to produce when saving the docklayout for later use.  I have hardcoded the ToolWindowsHaveCloseButtons="False" in my xaml which the issue arises.  I have 2 layers deep of Docksites and a tab control in-between.  On rare occasions, when saving off my Docksite, the required toolwindow which doesn't allow closing disappears.  The backend code is a little crazy thanks to all the noise WPF generates when creating controls so I've had to add some wacky rules to get the state to save and load correctly.  That all said, I can't get this bug to happen on my own so I can't create a sample app to expose the issue.  Here's the inner Docksite code that is running within a tab control:

<local:BaseDockableControl x:Class="ServerManager.TemplateTestPropertiesControl"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
	xmlns:docking="http://schemas.actiprosoftware.com/winfx/xaml/docking" 
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
	xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
	xmlns:lib="clr-namespace:ServerManagerLibrary;assembly=cbsm" 
	xmlns:local="clr-namespace:ServerManager" 
	mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="800">
	
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="*"/>
			<RowDefinition Height="Auto"/>
		</Grid.RowDefinitions>
		<docking:DockSite x:Name="dockSite" Grid.Row="0" Margin="0" Padding="0" 
			CanToolWindowsClose="False" 
			CanToolWindowsFloat="False" 
			ToolWindowsHaveTabImages="True" 
			ToolItemContainerStyle="{StaticResource ToolWindowStyle}"
			ToolItemTemplateSelector="{StaticResource ToolItemTemplateSelector}"
			ToolItemsSource="{Binding ToolViewModels}" 
			ToolWindowsHaveCloseButtons="False">
			
			<docking:SplitContainer Orientation="Vertical">
				<docking:Workspace>
					<ScrollViewer Grid.Row="0 " VerticalScrollBarVisibility="Auto" Background="{StaticResource White}">
						<Grid>
							<Grid.RowDefinitions>
								<RowDefinition Height="*"/>
							</Grid.RowDefinitions>
							<ContentPresenter Grid.Row="0" Content="{Binding}" ContentTemplateSelector="{StaticResource TemplateTestPropertiesSelector}"/>
						</Grid>
					</ScrollViewer>
				</docking:Workspace>
			</docking:SplitContainer>
		</docking:DockSite>
		<!--Nasty hack to prevent Ctrl+Left Mouse Click from moving focus to another tab-->
		<StackPanel Grid.Row="1" Focusable="True" />
	</Grid>
</local:BaseDockableControl>
using System.Diagnostics;
using System.IO;
using System.Windows;
using ActiproSoftware.Windows.Controls.Docking;
using ServerManagerLibrary;
using XPlatformShared;
namespace ServerManager {

    public abstract class BaseDockableControl : UserControl {

        protected abstract DockSite _dockSite { get; }
        //private DateTime? LastTimeLoaded;
        private DateTime? LastTimeSaved;
        int DuplicateWindowsOpeningCount;
        private static int Id;
        private readonly int _id;

        public BaseDockableControl() {
            _id = Id++;
            Debug.WriteLine($"{GetType().Name} {_id}");
            Loaded += UserControl_Loaded;
            Unloaded += UserControl_Unloaded;
        }

        protected virtual void UserControl_Loaded(object? sender, RoutedEventArgs e) {
            try {
                Debug.WriteLine($"{GetType().Name} UserControl_Loaded {_id}");
                LoadDockState();
            }
            catch { }
        }

        private void UserControl_Unloaded(object? sender, RoutedEventArgs e) {
            try {
                Debug.WriteLine($"{GetType().Name} UserControl_Unloaded {_id}");
                SaveDockState();
            }
            catch { }
        }

        private void _dockSite_WindowsOpening(object? sender, DockingWindowsEventArgs e) {
            try {
                Debug.WriteLine($"{GetType().Name} _dockSite_WindowsOpening {_id} {DuplicateWindowsOpeningCount}");
                if (DuplicateWindowsOpeningCount > 0)
                    return;
                DuplicateWindowsOpeningCount++;
                LoadDockState();
            }
            catch { }
        }

        private void _dockSite_WindowsClosing(object? sender, DockingWindowsEventArgs e) {
            try {
                Debug.WriteLine($"{GetType().Name} _dockSite_WindowsClosing {_id}");
                SaveDockState();
            }
            catch { }
        }

        #region State Filename        
        private string StateFileName {
            get {
                if (DataContext != null)
                    return $"{DataContext.GetType().Name}.Dock.xml";
                else
                    return $"{GetType().Name}.Dock.xml";
            }
        }

        private string StateFilePath => Path.Combine(CBPath.UserApplicationDataPath, StateFileName);
        #endregion

        private void LoadDockState() {
            //if (LastTimeLoaded != null && DateTime.Now - LastTimeLoaded < TimeSpan.FromSeconds(1))
            //    return;
            if (_dockSite == null)
                return;
            bool stateIsLoadedFromFile = false;
            try {
                _dockSite.WindowsOpening -= _dockSite_WindowsOpening;
                _dockSite.WindowsClosing -= _dockSite_WindowsClosing;
                if (File.Exists(StateFilePath)) {
#if DEBUG
                    var contents = File.ReadAllText(StateFilePath);
#endif
                    App.DockSiteLayoutSerializerInstance.LoadFromFile(StateFilePath, _dockSite);
                    stateIsLoadedFromFile = true;
                }
            }
            catch { }
            finally {
                _dockSite.WindowsOpening += _dockSite_WindowsOpening;
                _dockSite.WindowsClosing += _dockSite_WindowsClosing;
            }

            if (!stateIsLoadedFromFile) {
                if (_dockSite.ToolWindows.Count > 1)
                    _dockSite.ToolWindows[_dockSite.ToolWindows.Count - 1].Activate();
            }

            foreach (var toolWindow in _dockSite.ToolWindows) {
                if (toolWindow.DataContext is BaseViewModel toolWindowVM)
                    toolWindowVM.InitializationComplete();
            }

            if (DataContext is TabItemViewModel tabItemVM)
                tabItemVM.OnDockStateLoaded();
            Debug.WriteLine($"{GetType().Name} LoadDockState {_id}");

            //LastTimeLoaded = DateTime.Now;
        }

        public void SaveDockState() {
            if (_dockSite == null)
                return;
            if (_dockSite.ToolWindows.Count == 0)
                return;
            if (LastTimeSaved != null && DateTime.Now - LastTimeSaved < TimeSpan.FromSeconds(1))
                return;
            var xml = App.DockSiteLayoutSerializerInstance.SaveToString(_dockSite);
            XFile.StringToFile(xml, StateFilePath);
            Debug.WriteLine($"{GetType().Name} SaveDockState {_id}");
            LastTimeSaved = DateTime.Now;
        }

        protected void OnPreviewNumericText(object? sender, System.Windows.Input.TextCompositionEventArgs e) {
            e.Handled = RegExUtil.IsNumeric(e.Text);
        }
    }
}

[Modified 7 months ago]

Comments (4)

Posted 7 months ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

It's hard to say with little to go on, but I would suspect that trying to load/save layouts within events like WindowsOpening/Closing event handlers is likely contributing to issues since those fire in the midst of layout changes.  Plus it looks like you've added all kinds of logic to work around issues introduced by doing a load/save layout in those scenarios.

What happens if you ONLY load/save layout in the Loaded/Unloaded event handlers of the UserControl?  Does it work better then?

What is the reasoning behind loading and saving as docking windows open/close versus at more spaced out times such as when the app (or at least your root Window) opens or closes?

It helps with large samples like this if you use the "Insert code sample" button when pasting code.  Otherwise it comes in completely unformatted and is very difficult to read.  We spent a while reformatting the code above so it would be readable.  Thanks!


Actipro Software Support

Posted 7 months ago by Michael Janulaitis - Corner Bowl Software
Avatar

I recall when I had the code in the loaded and unloaded, the views were whiped out and the saved state was wrong.  It would save state with no controls.  Also I have embeded dockable views within other dockable views and the reside in tabs.  the state was lost without the _dockSite_WindowsOpening and _dockSite_WindowsClosing.  This was the only way I could get the state to work 99% of the time.  To duplicate you can create a dockable layout.  In one of the documents add a tab control.  Then within one of the tabs create another dockable view.  Then try flipping bewteen different documents, different tabs within each document, moving the child dock windows around then flipping back and forth between either tabs or documents.  This scenario is why I wrote the wacky code.  Since I use databinding the toolwindows are destroyed in my view models which are in a .net core library and have no understanding of any type of user interface.  I wrote a sample years ago that showed this bug off and submitted it. It should be in your archive somewhere.  I found the sample.  I will upload shortly after I update the nuget packages and my license.

[Modified 7 months ago]

Posted 7 months ago by Michael Janulaitis - Corner Bowl Software
Avatar

I removed the windows opening and window closing code and now there are no tool windows when the view is unloaded.  So I can't save the state.

Answer - Posted 7 months ago by Michael Janulaitis - Corner Bowl Software
Avatar

I modified my code to save off the state when the datacontext is lost.  I also removed the code where I nulled out the observable toolwindow array.  I now, dispose of the toolwindow view models, but leave them in the array.  So far this seems to be working well:

        #region Constructor
        public BaseDockableControl()
        {
            Loaded += UserControl_Loaded;            
            DataContextChanged += BaseDockableControl_DataContextChanged;
        }
        #endregion

        #region Event Handlers
        protected virtual void UserControl_Loaded(object? sender, RoutedEventArgs e)
        {
            try
            {
                Loaded -= UserControl_Loaded;

                LoadDockState();                
            }
            catch { }
        }

        private void BaseDockableControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            try
            {
                if (e.NewValue == null)
                    SaveDockState();
            }
            catch { }
        }
        #endregion

[Modified 7 months ago]

The latest build of this product (v24.1.5) was released 5 days ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.