Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions docfx/articles/dock-active-document.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
# Finding the Active Document

Only one dockable (document or tool) can have focus at a time, even when multiple
`IDocumentDock` instances or floating windows are present. The root dock that owns
the focused dockable stores it in `FocusedDockable`, and the factory reports focus
changes through `IFactory.FocusedDockableChanged`.
`IDocumentDock` instances or floating windows are present.

Use factory global tracking for cross-window scenarios:

- `IFactory.CurrentDockable`
- `IFactory.CurrentRootDock`
- `IFactory.CurrentDockWindow`
- `IFactory.GlobalDockTrackingChanged`

If you already have the root layout for a window, read `FocusedDockable` directly:

```csharp
var currentDocument = Layout?.FocusedDockable as IDocument;
```

To track focus across multiple windows, subscribe to the factory event and cache
the latest dockable:
To track focus across multiple windows, subscribe to global tracking:

```csharp
IDockable? focusedDockable = null;

factory.FocusedDockableChanged += (_, e) =>
factory.GlobalDockTrackingChanged += (_, e) =>
{
focusedDockable = e.Dockable;
Console.WriteLine($"Focused dockable: {e.Dockable?.Title}");
focusedDockable = e.Current.Dockable;
Console.WriteLine($"Focused dockable: {e.Current.Dockable?.Title}");
};

var currentDocument = focusedDockable as IDocument;
```

If you need the root dock that owns the focused dockable:
If you need the owning root/window:

```csharp
var focusedRoot = factory.CurrentRootDock;
var focusedWindow = factory.CurrentDockWindow;
```

You can also use helper extensions:

```csharp
var focusedRoot = focusedDockable is { } dockable
? factory.FindRoot(dockable, root => root.IsFocusableRoot)
: null;
var currentDocument = factory.GetCurrentDocument();
var activeRoot = factory.GetActiveRoot();
```

```csharp
Expand All @@ -42,6 +52,4 @@ if (focusedDockable is { } dockable)
}
```

The focused dockable can be `null` when nothing is focused. In multi-window
layouts, each root keeps its last focused dockable, so use the event to determine
the current focus.
The focused dockable can be `null` when nothing is focused.
2 changes: 1 addition & 1 deletion docfx/articles/dock-api-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ dockControl.Factory = factory;
dockControl.Layout = layout;
```

These factories expose methods such as `AddDockable`, `MoveDockable` or `FloatDockable` and raise events like `ActiveDockableChanged` so you can react to changes.
These factories expose methods such as `AddDockable`, `MoveDockable` or `FloatDockable` and raise events like `ActiveDockableChanged`. For cross-window state, use `GlobalDockTrackingChanged` and `CurrentDockable`/`CurrentRootDock`.

Use the MVVM, ReactiveUI or XAML samples as references for complete implementations.

Expand Down
10 changes: 7 additions & 3 deletions docfx/articles/dock-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
Dock exposes a large set of runtime events through `FactoryBase` so that applications can react to changes in the layout. The other guides only briefly mention these hooks. This document lists the most commonly used events and shows how to subscribe to them.

Each event uses a dedicated arguments type that exposes the affected
dockable or window. Some events (such as `DockableClosing` and `WindowClosing`)
support cancellation via a `Cancel` flag.
dockable or window. Focus/active dockable args also include `RootDock`,
`Window`, and `HostWindow` for multi-window context. Some events (such as
`DockableClosing` and `WindowClosing`) support cancellation via a `Cancel` flag.

## Common events

Expand All @@ -14,6 +15,7 @@ support cancellation via a `Cancel` flag.
| ----- | ----------- |
| `ActiveDockableChanged` | Fired when the active dockable within a dock changes. |
| `FocusedDockableChanged` | Triggered when focus moves to another dockable. |
| `GlobalDockTrackingChanged` | Fired when global dock/root/window context changes. |
| `DockableAdded` | Raised after a dockable is inserted into a dock. |
| `DockableRemoved` | Raised after a dockable has been removed. |
| `DockableClosing` | Fired before a dockable is closed so it can be cancelled. |
Expand Down Expand Up @@ -48,7 +50,9 @@ Create a factory instance and attach handlers before initializing the layout:
var factory = new Factory();

factory.ActiveDockableChanged += (_, args) =>
Console.WriteLine($"Active dockable: {args.Dockable?.Title}");
Console.WriteLine($"Active dockable: {args.Dockable?.Title} (root={args.RootDock?.Id}, window={args.Window?.Id})");
factory.GlobalDockTrackingChanged += (_, args) =>
Console.WriteLine($"Global context: {args.Current.Dockable?.Title} / {args.Current.RootDock?.Id} / {args.Current.Window?.Id}");
factory.DockableAdded += (_, args) =>
Console.WriteLine($"Added: {args.Dockable?.Title}");
factory.DockableDocked += (_, args) =>
Expand Down
76 changes: 76 additions & 0 deletions docfx/articles/dock-global-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Global Dock Tracking

This document defines the global tracking model introduced for multi-window focus and active document resolution.

## Problem

Per-dock events (`ActiveDockableChanged`, `FocusedDockableChanged`) previously exposed only a dockable reference. In multi-window layouts this made it hard to reliably identify:

- which root dock currently owns focus
- which floating dock window is currently active
- where global overlays (busy/dialog) should render

## Expected behavior

Global tracking should always resolve the current dock context across all windows:

- Current dockable (document/tool)
- Current root dock
- Current dock window (when floating)
- Current host window

## API

`IFactory` now exposes global state:

- `GlobalDockTrackingState GlobalDockTrackingState`
- `IDockable? CurrentDockable`
- `IRootDock? CurrentRootDock`
- `IDockWindow? CurrentDockWindow`
- `IHostWindow? CurrentHostWindow`

`IFactory` now exposes a global state event:

- `GlobalDockTrackingChanged`

`ActiveDockableChangedEventArgs` and `FocusedDockableChangedEventArgs` now include:

- `RootDock`
- `Window`
- `HostWindow`

## Tracking rules

State is updated from window/focus/activation transitions:

1. `FocusedDockableChanged` updates global state to the focused dockable context.
2. `WindowActivated` updates global state to that window plus its focused/active dockable.
3. `DockableActivated` updates global state only when the activation belongs to the tracked root (or no root is tracked yet).
4. `WindowDeactivated` clears state when the deactivated window is the tracked window.
5. `WindowClosed` and `WindowRemoved` clear state when they affect the tracked window.

`ActiveDockableChanged` and `DockableActivated` still raise normally, but global state only updates when the change belongs to the already tracked root (or no root is tracked yet). This avoids background roots overriding global context.

## Active document helpers

`FactoryExtensions` now uses global tracking first:

- `GetActiveRoot()` -> `CurrentRootDock` fallback to legacy `IsActive` scan.
- `GetCurrentDocument()` -> `CurrentDockable` fallback to focused dockable on active root.
- `CloseFocusedDockable()` closes `CurrentDockable` first.

## Overlay host resolution improvements

`IHostOverlayServicesProvider` now supports dockable-aware resolution:

```csharp
IHostOverlayServices GetServices(IScreen screen, IDockable dockable);
```

This allows busy/dialog/confirmation overlays to resolve by the dockable's current root/window, with `screen` used as fallback.

`RoutableDocumentBase` and `RoutableToolBase` use this dockable-aware overload by default.

## Sample status bar

`DockMvvmSample` and `DockReactiveUISample` include a status bar bound to global tracking state and updated through `GlobalDockTrackingChanged`.
10 changes: 10 additions & 0 deletions docfx/articles/dock-overlay-services-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Use this interface as the backing type for view models or overlay bindings inste
### Contracts
- `IHostServiceResolver` (namespace `Dock.Model.ReactiveUI.Services.Overlays.Hosting`) resolves services scoped to the current `IScreen`.
- `IHostOverlayServicesProvider` (namespace `Dock.Model.ReactiveUI.Services.Overlays.Hosting`) returns the `IHostOverlayServices` instance for a host screen.
- `IHostOverlayServicesProvider.GetServices(IScreen, IDockable)` resolves overlays using dockable context first, then falls back to host screen resolution.

### Default resolver
`OwnerChainHostServiceResolver` (namespace `Dock.Model.ReactiveUI.Services.Overlays.Hosting`) resolves services in this order:
Expand All @@ -59,6 +60,15 @@ This keeps overlays bound to the correct host window when dockables are moved or
### Provider behavior
`HostOverlayServicesProvider` (namespace `Dock.Model.ReactiveUI.Services.Overlays.Hosting`) uses the resolver and falls back to a cached per-screen instance when no host service is found. This keeps overlays functional in screens that are not attached to a dock layout yet.

Dockable-aware resolution:

```csharp
var overlays = provider.GetServices(hostScreen, dockable);
var busy = overlays.Busy;
```

Use this overload for busy/dialog actions that should render in the dockable's current window/root, especially when documents can move between floating windows.

## Overlay controls integration
Overlay controls bind directly to the service contracts:
- `BusyOverlayControl`: `BusyService`, `GlobalBusyService`.
Expand Down
2 changes: 2 additions & 0 deletions docfx/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
items:
- name: Active document
href: dock-active-document.md
- name: Global dock tracking
href: dock-global-tracking.md
- name: Docking groups
href: dock-docking-groups.md
- name: MDI document layout
Expand Down
29 changes: 27 additions & 2 deletions samples/DockMvvmSample/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ public class MainWindowViewModel : ObservableObject
{
private readonly IFactory? _factory;
private IRootDock? _layout;
private string _globalStatus = "Global: (none)";

public IRootDock? Layout
{
get => _layout;
set => SetProperty(ref _layout, value);
}

public string GlobalStatus
{
get => _globalStatus;
set => SetProperty(ref _globalStatus, value);
}

public ICommand NewLayout { get; }

public MainWindowViewModel()
Expand All @@ -33,6 +40,9 @@ public MainWindowViewModel()
_factory?.InitLayout(layout);
}
Layout = layout;
GlobalStatus = layout is null
? "Global: (none)"
: FormatGlobalStatus(_factory?.GlobalDockTrackingState ?? GlobalDockTrackingState.Empty);

if (Layout is { } root)
{
Expand Down Expand Up @@ -85,12 +95,18 @@ private void DebugFactoryEvents(IFactory factory)
{
factory.ActiveDockableChanged += (_, args) =>
{
Debug.WriteLine($"[ActiveDockableChanged] Title='{args.Dockable?.Title}'");
Debug.WriteLine($"[ActiveDockableChanged] Title='{args.Dockable?.Title}', Root='{args.RootDock?.Id}', Window='{args.Window?.Id}'");
};

factory.FocusedDockableChanged += (_, args) =>
{
Debug.WriteLine($"[FocusedDockableChanged] Title='{args.Dockable?.Title}'");
Debug.WriteLine($"[FocusedDockableChanged] Title='{args.Dockable?.Title}', Root='{args.RootDock?.Id}', Window='{args.Window?.Id}'");
};

factory.GlobalDockTrackingChanged += (_, args) =>
{
GlobalStatus = FormatGlobalStatus(args.Current);
Debug.WriteLine($"[GlobalDockTrackingChanged] Reason='{args.Reason}', Dockable='{args.Current.Dockable?.Title}', Root='{args.Current.RootDock?.Id}', Window='{args.Current.Window?.Id}'");
};

factory.DockableAdded += (_, args) =>
Expand Down Expand Up @@ -206,4 +222,13 @@ private void DebugFactoryEvents(IFactory factory)
Debug.WriteLine($"[DockableDeactivated] Title='{args.Dockable?.Title}'");
};
}

private static string FormatGlobalStatus(GlobalDockTrackingState state)
{
var dockableTitle = state.Dockable?.Title ?? "(none)";
var rootId = state.RootDock?.Id ?? "(none)";
var windowTitle = state.Window?.Title ?? "(main)";
var host = state.HostWindow?.GetType().Name ?? "(main)";
return $"Dockable: {dockableTitle} | Root: {rootId} | Window: {windowTitle} | Host: {host}";
}
}
22 changes: 14 additions & 8 deletions samples/DockMvvmSample/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
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:dm="using:Dock.Model.Core"
xmlns:dmc="using:Dock.Model.Controls"
xmlns:vm="using:DockMvvmSample.ViewModels"
mc:Ignorable="d"
Expand All @@ -16,7 +15,7 @@
<UserControl.Resources>
<StreamGeometry x:Key="DarkTheme">M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm0-2V4a8 8 0 1 1 0 16Z</StreamGeometry>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*,25" ColumnDefinitions="Auto,*" Background="Transparent">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*" Background="Transparent">
<Menu Grid.Row="0" Grid.Column="0" VerticalAlignment="Top">
<MenuItem Header="_File">
<MenuItem Header="_New Layout" Command="{Binding NewLayout}" />
Expand Down Expand Up @@ -71,11 +70,18 @@
</Panel>
<DockControl x:Name="DockControl" Layout="{Binding Layout}" Margin="4"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" />
<Panel DataContext="{Binding Layout.ActiveDockable}"
Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<TextBlock Text="{Binding FocusedDockable, FallbackValue={}}"
Margin="4"
x:DataType="dm:IDock" />
</Panel>
<Border Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4,0,4,4"
Padding="6,2"
Background="{DynamicResource DockThemeBackgroundLowBrush}"
BorderBrush="{DynamicResource DockThemeBorderMidBrush}"
BorderThickness="1"
CornerRadius="4">
<TextBlock Text="{Binding GlobalStatus}"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
</Border>
</Grid>
</UserControl>
29 changes: 27 additions & 2 deletions samples/DockReactiveUISample/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ public class MainWindowViewModel : ReactiveObject
{
private readonly IFactory? _factory;
private IRootDock? _layout;
private string _globalStatus = "Global: (none)";

public IRootDock? Layout
{
get => _layout;
set => this.RaiseAndSetIfChanged(ref _layout, value);
}

public string GlobalStatus
{
get => _globalStatus;
set => this.RaiseAndSetIfChanged(ref _globalStatus, value);
}

public ICommand NewLayout { get; }

public MainWindowViewModel()
Expand All @@ -36,6 +43,9 @@ public MainWindowViewModel()
layout.Navigate.Execute("Home");
}
Layout = layout;
GlobalStatus = layout is null
? "Global: (none)"
: FormatGlobalStatus(_factory?.GlobalDockTrackingState ?? GlobalDockTrackingState.Empty);

NewLayout = ReactiveCommand.Create(ResetLayout);
}
Expand All @@ -44,12 +54,18 @@ private void DebugFactoryEvents(IFactory factory)
{
factory.ActiveDockableChanged += (_, args) =>
{
Debug.WriteLine($"[ActiveDockableChanged] Title='{args.Dockable?.Title}'");
Debug.WriteLine($"[ActiveDockableChanged] Title='{args.Dockable?.Title}', Root='{args.RootDock?.Id}', Window='{args.Window?.Id}'");
};

factory.FocusedDockableChanged += (_, args) =>
{
Debug.WriteLine($"[FocusedDockableChanged] Title='{args.Dockable?.Title}'");
Debug.WriteLine($"[FocusedDockableChanged] Title='{args.Dockable?.Title}', Root='{args.RootDock?.Id}', Window='{args.Window?.Id}'");
};

factory.GlobalDockTrackingChanged += (_, args) =>
{
GlobalStatus = FormatGlobalStatus(args.Current);
Debug.WriteLine($"[GlobalDockTrackingChanged] Reason='{args.Reason}', Dockable='{args.Current.Dockable?.Title}', Root='{args.Current.RootDock?.Id}', Window='{args.Current.Window?.Id}'");
};

factory.DockableAdded += (_, args) =>
Expand Down Expand Up @@ -194,4 +210,13 @@ public void ResetLayout()
Layout = layout;
}
}

private static string FormatGlobalStatus(GlobalDockTrackingState state)
{
var dockableTitle = state.Dockable?.Title ?? "(none)";
var rootId = state.RootDock?.Id ?? "(none)";
var windowTitle = state.Window?.Title ?? "(main)";
var host = state.HostWindow?.GetType().Name ?? "(main)";
return $"Dockable: {dockableTitle} | Root: {rootId} | Window: {windowTitle} | Host: {host}";
}
}
Loading
Loading