XAML workbenches on UnoDock

UnoDock uses Uno/WinUI XAML rather than a second WPF parser. A DockingManager contains a LayoutRoot, panels/groups contain panes, and document/tool models contain content. The model remains separate from the native visual tree.

Declarative layout and content

<dock:DockingManager xmlns:dock="using:UnoDock"
    xmlns:layout="using:UnoDock.Layout" xmlns:themes="using:UnoDock.Themes"
    FloatingWindowMode="Native">
  <dock:DockingManager.Theme>
    <themes:FluentTheme RequestedTheme="Default" />
  </dock:DockingManager.Theme>
  <layout:LayoutRoot>
    <layout:LayoutPanel Orientation="Horizontal">
      <layout:LayoutAnchorablePane DockWidth="220" DockMinWidth="140">
        <layout:LayoutAnchorable Title="Explorer" ContentId="explorer" CanClose="False">
          <ListView />
        </layout:LayoutAnchorable>
      </layout:LayoutAnchorablePane>
      <layout:LayoutDocumentPane>
        <layout:LayoutDocument Title="Document.xaml" ContentId="document">
          <TextBox AcceptsReturn="True" TextWrapping="Wrap" />
        </layout:LayoutDocument>
      </layout:LayoutDocumentPane>
    </layout:LayoutPanel>
  </layout:LayoutRoot>
</dock:DockingManager>

The snippets assume the normal presentation and x: namespaces on their containing Page/UserControl. Compiled sample markup is in XamlWorkbenchView.xaml and XamlMvvmView.xaml. Both are available from the Gallery's Samples menu. The first declares panels, tools, a source-bound document, content/header templates, style selectors and native float/auto-hide/layout-serialization commands. The second declares observable DocumentsSource and AnchorablesSource collections, per-item bindings, document/tool templates, and live movement/hide/auto-hide/tabbed-document policies. Code-behind handles application commands; it does not construct the UI.

Model dependency properties

Native DP endpoints were added for content, tooltip, image, enabled/close/float policy, activation, selection, floating coordinates, document movement/description, tool hide/auto-hide/document-docking policy and auto-hide dimensions. Concrete panels, pane groups and panes also expose their dimensions, minimum dimensions, floating geometry, maximized/reposition/duplicate policy, orientation and selection where applicable. Root panels, all four root sides, root ActiveContent, floating-window root slots and anchorable visibility also have guarded native endpoints. There are 92 new endpoints across these model types; Title and ContentId keep their existing native properties. FloatingWindowMode and FluentTheme.RequestedTheme are now native DPs as well.

These endpoints invoke the existing public model setters, not parallel implementations of ownership, activation, pane selection or validation. CLR/model-originated changes publish to the DP, so TwoWay bindings receive them. Rejected/coerced writes are reconciled with the actual model even when a setter throws. The model's transaction and callback contracts in model-invariants.md still apply. Native DP notifications are sequential, not an atomic multi-property notification transport.

Use x:Bind for compiled Page/UserControl sources, or a normal Binding with an explicit Source for a nonvisual layout node. Do not assume WPF logical-tree DataContext inheritance on nonvisual objects. Read-only placement/state properties remain observable binding sources, not arbitrary writable DP targets. Structural slots invoke their existing ownership setters through their DP endpoints. Collections retain their get-only ownership collection; declare children inline or use DocumentsSource/AnchorablesSource rather than replacing the child list. WPF markup extensions, triggers and arbitrary WPF resources are not WinUI XAML and are not emulated.

Source-backed items and reference-library container conventions

The normal DocumentsSource, AnchorablesSource, LayoutItemTemplate, template selectors, header/title templates and LayoutItemContainerStyle entry points remain. LayoutItem.DataContext is its Model (the content payload); header templates receive the LayoutContent model. A content template therefore binds Title, while a header template can bind Title or Content.Title according to its intention.

Uno 6.7 compiled binding-valued Setter.Value expressions do not create independent bindings on layout items. Use the portable attached binding definitions instead:

<Style x:Key="DocumentStyle" TargetType="controls:LayoutItem">
  <Setter Property="controls:LayoutItemBindings.Bindings">
    <Setter.Value>
      <controls:LayoutItemBindingCollection>
        <controls:LayoutItemBinding Property="Title" Path="Title" Mode="TwoWay" />
        <controls:LayoutItemBinding Property="ContentId" Path="ContentId" />
        <controls:LayoutItemBinding Property="CanClose" Path="CanClose" />
      </controls:LayoutItemBindingCollection>
    </Setter.Value>
  </Setter>
</Style>

controls maps to using:UnoDock.Controls. Definitions support Path, Mode, UpdateSourceTrigger, Source, Converter, ConverterParameter, ConverterLanguage, FallbackValue and TargetNullValue. ElementName/RelativeSource are passed to the native binding engine; they do not create a namescope or visual ancestors for the nonvisual item. Prefer explicit sources for those cases. Definition collections are configuration snapshots: replace the attached collection/style to reconfigure an existing item, rather than mutating a used collection in place.

Every item receives a new native Binding. Local application bindings/values win. Old definition-owned bindings are released without clearing the model's state; source/content replacement follows the item DataContext. Invalid/duplicate targets are rejected before retiring the old bindings. Reentrant collection replacement is serialized with a bounded convergence check and cannot finish an obsolete plan. Literal container-style metadata and capability setters are synchronized to the model and default commands, rather than only changing the adapter's appearance. Container-style replacement is serialized too: an application callback which selects a newer style prevents stale literal policy values from being published.

Binding retirement and adapter lifetime

Retiring an owned binding or command can invoke application dependency-property callbacks. Cleanup snapshots and retires its ownership records before executing these callbacks, attempts every independent removal, and removes only expressions or commands whose current identity still matches that snapshot. A binding installed by application code during removal of another binding is not cleared by the old operation. The normal native binding engine continues to own that replacement.

Disposal becomes terminal before callbacks run. It independently attempts model unsubscription, menu disposal, definition/default-binding removal, command removal, view detachment and clearing of presenter content/template/data context. Retained presenter references are retired before callbacks can attempt to recreate a view. A single observer failure retains its identity and stack; multiple cleanup failures are reported in occurrence order. Repeated disposal is inert, even when the first call reported an error. Disposing an adapter does not dispose or clear its layout model's application-owned content payload.

A callback failure while replacing definitions does not roll back arbitrary application side effects or install a partially retired request as a successful replacement. All old owned bindings are attempted before the error is propagated; a subsequent explicit assignment of a new configuration can establish fresh bindings. Invalid preflight still leaves the previous bindings untouched. These are distinct failure boundaries. Fourteen public-API regressions cover document and tool adapters, failure aggregation, recovery and reentrant binding ownership. Two additional cases deliver a model event captured before disposal: a subscriber registered before the adapter disposes it first, and the remaining captured delivery must not repopulate its cleared Model, DataContext or presenter. The model itself retains its new application payload.

Theme and lightweight styling

Merge themes:WorkbenchResources in an owner scope, then use Style="{StaticResource UnoDock.WorkbenchManagerStyle}" with FluentTheme. The optional dictionary supplies independently authored Light/Dark brushes, 26-DIP tool captions, 28-DIP document tabs, 30-DIP rails, and 3-DIP chrome-button corners. The legacy default/Generic palette and compact metrics do not change.

FluentTheme.RequestedTheme can be set in XAML or changed/bound at runtime. Default follows the owning manager; explicit Light/Dark remains independent of the host. Managers subscribe only to their active theme and detach when replaced/disposed. Numeric UnoDock.* overrides now use the same owner/ancestor/application lookup as Fluent brushes. Manager resource overrides remain most specific. A Refresh applies ordinary ResourceDictionary replacements; dictionary edits are not a general observable-collection notification mechanism.

The default manager template now respects Background, BorderBrush, BorderThickness, CornerRadius and Padding. Custom templates retain PART_LayoutHost (ContentPresenter) and PART_AutoHideArea. Retemplating releases the previous host's surface before reattaching it, preserving owned content presenters and layout models. Border rounding is presentation; arbitrary child clipping is not implicitly added.

Density and selected-tab presentation

DockingManager.ChromeDensity is a bindable, validated native property. It alters retained chrome geometry, not layout ownership or editor identity. The optional WorkbenchManagerStyle selects Comfortable; unstyled managers retain Default.

Density Tool caption Document tab Tool tab Rail Icon button
Compact 18 20 23 26 16
Comfortable 26 28 26 30 20
Spacious 32 36 32 38 28

Sizes are DIPs, before explicit resource overrides and font-legibility floors. Default uses the original palette metrics and unqualified resources; the optional workbench dictionary's unqualified metrics remain backward compatible. Explicit profiles can be selected in markup or changed while tool/document windows are open:

<dock:DockingManager ChromeDensity="Spacious"
    Style="{StaticResource UnoDock.WorkbenchManagerStyle}">
  <dock:DockingManager.Theme>
    <themes:FluentTheme />
  </dock:DockingManager.Theme>
</dock:DockingManager>

For Fluent managers, a profile first considers UnoDock.Spacious.TitleHeight (for example), then UnoDock.TitleHeight in the same resource scope. A nearer unqualified application override therefore beats a profile default in an ancestor or merged workbench dictionary. Font scaling retains readable minimum geometry, and icon button sizes are bounded by their containing rows. Change ordinary resource values and call Refresh; dictionary replacement is not an automatic resource-notification system.

UnoDock.ActiveTabIndicatorThickness opts into a retained, non-hit-testable top indicator: the active selected tab uses the accent brush and an inactive selection uses the border brush. WorkbenchResources chooses 2 DIPs; legacy chrome defaults to zero and its geometry/palette stays unchanged. Rounded button geometry now also follows the workbench palette in native floating captions. It does not replace native windows, editors or their caption command handlers.

The compiled density picker drives the manager through an ElementName Binding. Both sample command strips scroll horizontally when constrained. The workbench's star-sized side panes and document policy labels remain usable at the exercised 600-DIP width; this is not a mobile/adaptive docking-layout contract.

Source-backed tool policy and reverse TwoWay binding

LayoutDocumentItem exposes CanMove. LayoutAnchorableItem exposes CanAutoHide and CanDockAsTabbedDocument as native binding/style targets, alongside CanHide. The MVVM sample declares a fixed initial tool pane and supplies its tools from AnchorablesSource. Its title editor and policy toggles are XAML templates, not code-created controls:

<Style x:Key="ToolStyle" TargetType="controls:LayoutAnchorableItem">
  <Setter Property="controls:LayoutItemBindings.Bindings">
    <Setter.Value>
      <controls:LayoutItemBindingCollection>
        <controls:LayoutItemBinding Property="Title" Path="Title" Mode="TwoWay" />
        <controls:LayoutItemBinding Property="CanHide" Path="CanHide" Mode="TwoWay" />
        <controls:LayoutItemBinding Property="CanAutoHide" Path="CanAutoHide" Mode="TwoWay" />
        <controls:LayoutItemBinding Property="CanDockAsTabbedDocument"
            Path="CanDockAsTabbedDocument" Mode="TwoWay" />
      </controls:LayoutItemBindingCollection>
    </Setter.Value>
  </Setter>
</Style>

These values update the real model and its command eligibility. Model-side edits now propagate back through explicitly TwoWay definition-owned bindings without replacing the Binding expression. OneWay definitions do not write the source, and a binding installed later by application code is not reclaimed. Synchronizing an owned binding also tolerates a callback replacing the definitions with another source. Command/menu invalidation is still attempted if binding synchronization raises an error; binding-engine conversion and exception behavior remain native.

Source selection must be unambiguous: specify at most one of Source, ElementName or RelativeSource. Empty target names and invalid binding-mode/update-trigger enums are rejected while preflighting the new definitions. Rejection leaves the previous owned bindings installed. Source removal releases adapter-owned expressions on normal manager reconciliation, not synchronously inside collection removal.

The workbench identifies its document by retained content identity and initializes its source ContentId consistently. This fixes the previous Float/Dock handler's search for an ID replaced by its own item style. Tests exercise the real button handler, save/restore and a subsequent float rather than only invoking model APIs.

Validation and scope

The registered xaml-workbench actual-host suite exercises compiled x:Bind, explicit native Binding, TwoWay model writes, invalid dimensions, reentrant activation, runtime XamlReader construction, model defaults, native/in-surface float/dock retention, light/dark resources, live metrics, retemplating, per-item binding ownership, literal capability setters and superseded binding definitions. The suite has 86 cases, including 16 binding/disposal cleanup regressions. Light/dark workbench and source-backed policy captures, plus a narrow workbench capture, are emitted under the suite's visuals. The sample action regressions invoke actual buttons through their native automation Invoke provider; these cases do not claim physical pointer coverage.

XamlReader is a trusted-markup feature, not an untrusted document sandbox. Runtime markup must use types present in the application's metadata; compiled samples also retain the relevant types for trimmed hosts. Browser publish and native WinUI package builds are not browser/native-WinUI input acceptance. High-contrast hardware, pure Wayland, mixed-DPI displays and physical AppKit pointer automation remain separate acceptance work. No full WPF XAML/binary/pixel equivalence is asserted.