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
117 changes: 114 additions & 3 deletions src/Dock.Serializer.Protobuf/ListTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Dock.Model.Controls;
using Dock.Model.Core;

namespace Dock.Serializer.Protobuf;

internal static class ListTypeConverter
{
public static void Convert(object? obj, Type listType)
{
Convert(obj, listType, new HashSet<object>(ReferenceEqualityComparer.Instance));
}

private static void Convert(object? obj, Type listType, HashSet<object> visited)
{
if (obj is null)
return;
if (!visited.Add(obj))
return;
if (obj is IEnumerable enumerable && obj is not string)
{
foreach (var item in enumerable)
{
Convert(item, listType);
Convert(item, listType, visited);
}
}
var type = obj.GetType();
Expand All @@ -39,14 +48,116 @@ public static void Convert(object? obj, Type listType)
}
foreach (var item in list)
{
Convert(item, listType);
Convert(item, listType, visited);
}
property.SetValue(obj, list);
}
else
{
Convert(value, listType);
Convert(value, listType, visited);
}
}

// protobuf-net cannot preserve references: reconcile the dockable aliases that came back
// as duplicates, and rebuild Owner (excluded from the wire) from tree containment.
if (obj is IDock dock)
{
Reconcile(dock, dock.ActiveDockable, v => dock.ActiveDockable = v);
Reconcile(dock, dock.DefaultDockable, v => dock.DefaultDockable = v);
Reconcile(dock, dock.FocusedDockable, v => dock.FocusedDockable = v);

SetOwner(dock.VisibleDockables, dock);

if (dock is IRootDock rootDock)
{
SetOwner(rootDock.HiddenDockables, rootDock);
SetOwner(rootDock.LeftPinnedDockables, rootDock);
SetOwner(rootDock.RightPinnedDockables, rootDock);
SetOwner(rootDock.TopPinnedDockables, rootDock);
SetOwner(rootDock.BottomPinnedDockables, rootDock);

if (rootDock.PinnedDock is { } pinnedDock)
{
pinnedDock.Owner = rootDock;
}

if (rootDock.Windows is not null)
{
foreach (var window in rootDock.Windows)
{
window.Owner = rootDock;
}
}
}

if (dock is ISplitViewDock splitViewDock)
{
if (splitViewDock.PaneDockable is { } paneDockable)
{
paneDockable.Owner = dock;
}

if (splitViewDock.ContentDockable is { } contentDockable)
{
contentDockable.Owner = dock;
}
}
}
}

private static void SetOwner(IList<IDockable>? dockables, IDockable owner)
{
if (dockables is null)
{
return;
}

foreach (var dockable in dockables)
{
dockable.Owner = owner;
}
}

private static void Reconcile(IDock dock, IDockable? current, Action<IDockable> setter)
{
if (current is null || string.IsNullOrEmpty(current.Id))
{
return;
}

var canonical = FindById(dock.VisibleDockables, current.Id)
?? (dock is IRootDock rootDock ? FindById(rootDock.HiddenDockables, current.Id) : null);

if (canonical is not null && !ReferenceEquals(canonical, current))
{
setter(canonical);
}
}

private static IDockable? FindById(IList<IDockable>? dockables, string id)
{
if (dockables is null)
{
return null;
}

IDockable? match = null;
foreach (var dockable in dockables)
{
if (dockable.Id != id)
{
continue;
}

if (match is not null)
{
// Ambiguous: more than one dockable shares this Id, so we cannot pick the alias safely.
return null;
}

match = dockable;
}

return match;
}
}
45 changes: 41 additions & 4 deletions src/Dock.Serializer.Protobuf/ProtobufDockSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ private static RuntimeTypeModel BuildModel()
var interfaceHierarchy = BuildInterfaceHierarchy(dockInterfaces);
var classHierarchy = BuildClassHierarchy(dockInterfaces, dockClasses);

foreach (var baseType in dockInterfaces)
var baseTypes = new HashSet<Type>(dockInterfaces);
baseTypes.UnionWith(classHierarchy.Keys);

foreach (var baseType in baseTypes)
{
var derivedTypes = new List<Type>();
if (interfaceHierarchy.TryGetValue(baseType, out var interfaceTypes))
Expand Down Expand Up @@ -225,21 +228,41 @@ private static Dictionary<Type, List<Type>> BuildClassHierarchy(
IReadOnlyCollection<Type> dockInterfaces,
IReadOnlyCollection<Type> dockClasses)
{
var classSet = new HashSet<Type>(dockClasses);
var map = new Dictionary<Type, List<Type>>();
foreach (var dockClass in dockClasses)
{
var baseInterface = GetClosestDockInterface(dockClass, dockInterfaces);
if (baseInterface is null)
// A subclass must be registered under its nearest concrete Dock base class, not just an
// interface, or protobuf-net's base-chain dispatch throws "Unexpected sub-type".
var registrationBase = FindNearestTrackedBaseClass(dockClass, classSet)
?? GetClosestDockInterface(dockClass, dockInterfaces);
if (registrationBase is null)
{
continue;
}

AddToTypeMap(map, baseInterface, dockClass);
AddToTypeMap(map, registrationBase, dockClass);
}

return map;
}

private static Type? FindNearestTrackedBaseClass(Type type, IReadOnlyCollection<Type> classSet)
{
var current = type.BaseType;
while (current is not null && current != typeof(object))
{
if (classSet.Contains(current))
{
return current;
}

current = current.BaseType;
}

return null;
}

private static Type? GetClosestDockInterface(Type type, IReadOnlyCollection<Type> dockInterfaces)
{
var candidates = type.GetInterfaces()
Expand Down Expand Up @@ -353,6 +376,15 @@ private static void ConfigureClass(RuntimeTypeModel model, Type type)
}
}

private static bool IsBackReferenceProperty(PropertyInfo property)
{
// Owner/OriginalOwner close the reference cycles that protobuf-net 3.x cannot preserve
// (AsReference is obsolete-as-error), so they are excluded from the wire; ListTypeConverter
// rebuilds Owner from tree containment after deserializing.
return (property.Name == nameof(IDockable.Owner) || property.Name == nameof(IDockable.OriginalOwner))
&& typeof(IDockable).IsAssignableFrom(property.PropertyType);
}

private static IReadOnlyList<PropertyInfo> GetSerializableProperties(Type type)
{
var properties = new List<PropertyInfo>();
Expand All @@ -373,6 +405,11 @@ private static IReadOnlyList<PropertyInfo> GetSerializableProperties(Type type)
continue;
}

if (IsBackReferenceProperty(property))
{
continue;
}

properties.Add(property);
}

Expand Down
74 changes: 74 additions & 0 deletions src/Dock.Serializer.SystemTextJson/DockListTypeInfoModifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization.Metadata;
using Dock.Model.Core;

namespace Dock.Serializer.SystemTextJson;

/// <summary>
/// Resolver modifier that makes <see cref="IList{T}"/> properties deserialize into a
/// configured concrete list type without replacing the built-in enumerable path.
/// </summary>
internal static class DockListTypeInfoModifier
{
public static void Apply(JsonTypeInfo typeInfo, Type listType)
{
if (typeInfo.Kind != JsonTypeInfoKind.Enumerable)
{
return;
}

var type = typeInfo.Type;
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(IList<>))
{
return;
}

if (TryAssignAotSafeCreator(typeInfo, type, listType))
{
return;
}

var elementType = type.GetGenericArguments()[0];
var concreteListType = listType.MakeGenericType(elementType);
typeInfo.CreateObject = () => Activator.CreateInstance(concreteListType)!;
}

private static bool TryAssignAotSafeCreator(JsonTypeInfo typeInfo, Type type, Type listType)
{
if (listType == typeof(ObservableCollection<>))
{
if (type == typeof(IList<IDockable>))
{
typeInfo.CreateObject = static () => new ObservableCollection<IDockable>();
return true;
}

if (type == typeof(IList<IDockWindow>))
{
typeInfo.CreateObject = static () => new ObservableCollection<IDockWindow>();
return true;
}
}

if (listType == typeof(List<>))
{
if (type == typeof(IList<IDockable>))
{
typeInfo.CreateObject = static () => new List<IDockable>();
return true;
}

if (type == typeof(IList<IDockWindow>))
{
typeInfo.CreateObject = static () => new List<IDockWindow>();
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ public static JsonSerializerOptions Create(Type listType, IJsonTypeInfoResolver
ReferenceHandler = ReferenceHandler.Preserve,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
TypeInfoResolver = typeInfoResolver
TypeInfoResolver = typeInfoResolver.WithAddedModifier(typeInfo => DockListTypeInfoModifier.Apply(typeInfo, listType))
};

options.Converters.Add(new JsonConverterFactoryList(listType));
return options;
}
}
46 changes: 0 additions & 46 deletions src/Dock.Serializer.SystemTextJson/JsonConverterFactoryList.cs

This file was deleted.

Loading