// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.CodeDom;
using System.Collections;
namespace System.ComponentModel.Design.Serialization;
public sealed partial class CodeDomComponentSerializationService
{
private sealed partial class CodeDomSerializationStore
{
/// <summary>
/// This is a simple code dom serializer that serializes a set of objects as a unit.
/// </summary>
private class ComponentListCodeDomSerializer : CodeDomSerializer
{
internal static readonly ComponentListCodeDomSerializer s_instance = new();
private readonly Dictionary<string, OrderedCodeStatementCollection?> _statementsTable = [];
private readonly Dictionary<string, List<CodeExpression>> _expressions = [];
private Dictionary<string, CodeDomComponentSerializationState>? _objectState; // only used during deserialization
private bool _applyDefaults = true;
private readonly HashSet<string> _nameResolveGuard = [];
public override object Deserialize(IDesignerSerializationManager manager, object state)
{
throw new NotSupportedException();
}
private static void PopulateCompleteStatements(object? data, string name, CodeStatementCollection completeStatements, Dictionary<string, List<CodeExpression>> expressions)
{
if (data is null)
{
return;
}
if (data is CodeStatementCollection statements)
{
completeStatements.AddRange(statements);
}
else if (data is CodeStatement statement)
{
completeStatements.Add(statement);
}
else if (data is CodeExpression expression)
{
// we handle expressions a little differently since they don't have a LHS or RHS
// they won't show up correctly in the statement table. We will deserialize them explicitly.
if (!expressions.TryGetValue(name, out List<CodeExpression>? exps))
{
exps = [];
expressions[name] = exps;
}
exps.Add(expression);
}
else
{
Debug.Fail($"No case for {data.GetType().Name}");
}
}
/// <summary>
/// Deserializes the given object state. The results are contained within the serialization manager's
/// name table. The objectNames list is used to deserialize in the proper order,
/// as objectState is unordered.
/// </summary>
internal void Deserialize(IDesignerSerializationManager manager, Dictionary<string, CodeDomComponentSerializationState> objectState, List<string> objectNames, bool applyDefaults)
{
CodeStatementCollection completeStatements = [];
_expressions.Clear();
_applyDefaults = applyDefaults;
foreach (string name in objectNames)
{
if (objectState.TryGetValue(name, out CodeDomComponentSerializationState? state))
{
PopulateCompleteStatements(state.Code, name, completeStatements, _expressions);
PopulateCompleteStatements(state.Context, name, completeStatements, _expressions);
}
}
CodeStatementCollection mappedStatements = [];
CodeMethodMap methodMap = new(mappedStatements);
methodMap.Add(completeStatements);
methodMap.Combine();
_statementsTable.Clear();
// generate statement table keyed on component name
FillStatementTable(manager, _statementsTable, mappedStatements);
// We need to also ensure that for every entry in the statement table we have a
// corresponding entry in objectNames. Otherwise, we won't deserialize completely.
HashSet<string> completeNames = [..objectNames];
completeNames.UnionWith(_statementsTable.Keys);
_objectState = new(objectState);
ResolveNameEventHandler resolveNameHandler = new(OnResolveName);
manager.ResolveName += resolveNameHandler;
try
{
foreach (string name in completeNames)
{
ResolveName(manager, name, true);
}
}
finally
{
_objectState = null;
manager.ResolveName -= resolveNameHandler;
}
}
private void OnResolveName(object? sender, ResolveNameEventArgs e)
{
string name = e.Name!;
// note: this recursion guard does not fix the problem,
// but rather avoids a stack overflow which will bring down VS and cause loss of data.
if (!_nameResolveGuard.Add(name))
{
return;
}
try
{
IDesignerSerializationManager manager = (IDesignerSerializationManager)sender!;
if (ResolveName(manager, name, false))
{
e.Value = manager.GetInstance(name);
}
}
finally
{
_nameResolveGuard.Remove(name);
}
}
private void DeserializeDefaultProperties(IDesignerSerializationManager manager, string name, List<string>? defProps)
{
// Next, default properties, but only if we successfully resolved.
if (defProps is null || !_applyDefaults)
{
return;
}
object? comp = manager.GetInstance(name);
if (comp is null)
{
return;
}
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);
foreach (string propName in defProps)
{
PropertyDescriptor? prop = props[propName];
if (prop is not null && prop.CanResetValue(comp))
{
// If there is a member relationship setup for this property, we should disconnect it first.
// This makes sense, since if there was a previous relationship,
// we would have serialized it and not come here at all.
if (manager.TryGetService(out MemberRelationshipService? relationships) && relationships[comp, prop] != MemberRelationship.Empty)
{
relationships[comp, prop] = MemberRelationship.Empty;
}
prop.ResetValue(comp);
}
}
}
private static void DeserializeDesignTimeProperties(IDesignerSerializationManager manager, string name, Dictionary<string, object?>? state)
{
if (state is null)
{
return;
}
object? comp = manager.GetInstance(name);
if (comp is null)
{
return;
}
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(comp);
foreach (KeyValuePair<string, object?> stateEntry in state)
{
PropertyDescriptor? prop = props[stateEntry.Key];
prop?.SetValue(comp, stateEntry.Value);
}
}
/// <summary>
/// This is used to resolve nested component references.
/// NestedComponents don't exist as sited components within the DesignerHost,
/// they are actually sited within a parent component.
/// This method takes the FullName defined on INestedSite and returns the component which matches it.
/// outerComponent is the name of the topmost component which does exist in the DesignerHost
/// This code also exists in VSCodeDomDesignerLoader -- please keep them in sync.
/// </summary>
private static IComponent? ResolveNestedName(IDesignerSerializationManager? manager, string name, [NotNullIfNotNull(nameof(manager))] out string? outerComponent)
{
if (manager is null)
{
outerComponent = null;
return null;
}
bool moreChunks;
// We need to resolve the first chunk using the manager. other chunks will be resolved within the nested containers.
int curIndex = name.IndexOf('.');
Debug.Assert(curIndex > 0, "ResolvedNestedName accepts only nested names!");
outerComponent = name[..curIndex];
IComponent? curComp = manager.GetInstance(outerComponent) as IComponent;
do
{
int prevIndex = curIndex;
curIndex = name.IndexOf('.', curIndex + 1);
moreChunks = curIndex != -1;
string compName = moreChunks
? name.Substring(prevIndex + 1, curIndex)
: name[(prevIndex + 1)..];
if (string.IsNullOrEmpty(compName))
{
return null;
}
ISite? site = curComp?.Site;
if (!site.TryGetService(out INestedContainer? container))
{
return null;
}
curComp = container.Components[compName];
}
while (moreChunks);
return curComp;
}
private bool ResolveName(IDesignerSerializationManager manager, string name, bool canInvokeManager)
{
bool resolved = false;
// Check for a nested name. Components that are sited within NestedContainers need to be looked up
// in their nested container, and won't be resolvable directly via the manager.
if (name.IndexOf('.') > 0)
{
IComponent? nestedComp = ResolveNestedName(manager, name, out string? parentName);
if (nestedComp is not null && parentName is not null)
{
manager.SetName(nestedComp, name);
// What is the point of this?
// Well, the nested components won't be in the statement table with its nested name.
// However, their most parent component will be, so forcing a resolve of their name
// will actually deserialize the nested statements.
ResolveName(manager, parentName, canInvokeManager);
}
else
{
Debug.Fail($"Unable to resolve nested component: {name}");
}
}
// First we check to see if the statements table contains an OrderedCodeStatementCollection for this name.
// If it does this means we have not resolved this name yet, so we grab its
// OrderedCodeStatementCollection and deserialize that, along with any default properties
// and design-time properties.
// If it doesn't contain an OrderedCodeStatementsCollection this means one of two things:
// 1. We already resolved this name and shoved an instance in there.
// In this case we just return the instance
// 2. There are no statements corresponding to this name,
// but there might be expressions that have never been deserialized,
// so we check for that and deserialize those.
_statementsTable.TryGetValue(name, out OrderedCodeStatementCollection? statements);
if (statements is not null)
{
_statementsTable[name] = null; // prevent recursion
// we look through the statements to find the variableRef or fieldRef that matches this name
string? typeName = null;
foreach (CodeStatement statement in statements)
{
if (statement is CodeVariableDeclarationStatement codeVariableDeclaration)
{
typeName = codeVariableDeclaration.Type.BaseType;
break;
}
}
// next, invoke the serializer for this component
if (typeName is not null)
{
Type? type = manager.GetType(typeName);
if (type is null)
{
manager.ReportError(new CodeDomSerializerException(string.Format(SR.SerializerTypeNotFound, typeName), manager));
}
else if (statements.Count > 0)
{
CodeDomSerializer? serializer = GetSerializer(manager, type);
if (serializer is null)
{
// We report this as an error. This indicates that there are code statements
// in initialize component that we do not know how to load.
manager.ReportError(new CodeDomSerializerException(
string.Format(SR.SerializerNoSerializerForComponent, type.FullName), manager));
}
else
{
try
{
object? instance = serializer.Deserialize(manager, statements);
resolved = instance is not null;
if (resolved)
{
_statementsTable[name] = (OrderedCodeStatementCollection?)instance;
}
}
catch (Exception ex)
{
manager.ReportError(ex);
}
}
}
}
// if we can't find a typeName to get a serializer with we fallback to deserializing
// each statement individually using the default serializer.
else
{
foreach (CodeStatement cs in statements)
{
DeserializeStatement(manager, cs);
}
resolved = true;
}
if (_objectState!.Remove(name, out CodeDomComponentSerializationState? state))
{
DeserializeState(manager, name, state);
}
if (_expressions.Remove(name, out List<CodeExpression>? exps))
{
foreach (CodeExpression exp in exps)
{
DeserializeExpression(manager, name, exp);
}
resolved = true;
}
}
else
{
resolved = ((IDictionary)_statementsTable)[name] is not null;
if (!resolved)
{
// this is condition 2 of the comment at the start of this method.
if (_expressions.TryGetValue(name, out List<CodeExpression>? exps))
{
foreach (CodeExpression exp in exps)
{
object? exValue = DeserializeExpression(manager, name, exp);
if (exValue is not null && !resolved && canInvokeManager && manager.GetInstance(name) is null)
{
manager.SetName(exValue, name);
resolved = true;
}
}
}
// Sometimes components won't be in either the statements table or the expressions table,
// for example, this occurs for resources during undo/redo.
// In these cases the component should be resolvable by the manager.
// Never do this when we have been asked by the serialization manager to resolve the name;
// otherwise we may infinitely recurse.
if (!resolved && canInvokeManager)
{
resolved = manager.GetInstance(name) is not null;
}
// In this case we still need to correctly deserialize default properties & design-time only properties.
if (resolved && _objectState!.TryGetValue(name, out CodeDomComponentSerializationState? state))
{
DeserializeState(manager, name, state);
}
}
if (!resolved && canInvokeManager)
{
manager.ReportError(new CodeDomSerializerException(string.Format(SR.CodeDomComponentSerializationServiceDeserializationError, name), manager));
Debug.Fail($"No statements or instance for name and no lone expressions: {name}");
}
}
return resolved;
}
private void DeserializeState(IDesignerSerializationManager manager, string name, CodeDomComponentSerializationState state)
{
DeserializeDefaultProperties(manager, name, state.Properties);
DeserializeDesignTimeProperties(manager, name, state.Resources);
DeserializeEventResets(manager, name, state.Events);
DeserializeModifier(manager, name, state.Modifier);
}
private static void DeserializeEventResets(IDesignerSerializationManager? manager, string name, List<string>? eventNames)
{
if (eventNames is not null && manager is not null && !string.IsNullOrEmpty(name))
{
object? comp = manager.GetInstance(name);
if (comp is not null && manager.GetService(typeof(IEventBindingService)) is IEventBindingService ebs)
{
PropertyDescriptorCollection eventProps = ebs.GetEventProperties(TypeDescriptor.GetEvents(comp));
if (eventProps is not null)
{
foreach (string eventName in eventNames)
{
PropertyDescriptor? prop = eventProps[eventName];
prop?.SetValue(comp, null);
}
}
}
}
}
private static void DeserializeModifier(IDesignerSerializationManager manager, string name, object? state)
{
if (state is null)
{
return;
}
Debug.Assert(state is MemberAttributes, "Attempting to deserialize a null modifier");
object? comp = manager.GetInstance(name);
if (comp is not null)
{
MemberAttributes modifierValue = (MemberAttributes)state;
PropertyDescriptor? modifierProp = TypeDescriptor.GetProperties(comp)["Modifiers"];
modifierProp?.SetValue(comp, modifierValue);
}
}
public override object Serialize(IDesignerSerializationManager manager, object state)
{
throw new NotSupportedException();
}
/// <summary>
/// For everything in the serialization manager's container, we need a variable ref,
/// just in case something that has changed has a reference to another object. We also
/// must do this for everything that we are serializing that is not marked as EntireObject.
/// Otherwise reference could leak and cause the entire object to be serialized.
/// </summary>
internal void SetupVariableReferences(IDesignerSerializationManager manager, IContainer container, Dictionary<object, ObjectData> objectData, IList shimObjectNames)
{
foreach (IComponent c in container.Components)
{
string? name = TypeDescriptor.GetComponentName(c);
if (!string.IsNullOrEmpty(name))
{
bool needVar = !(objectData.TryGetValue(c, out ObjectData? data) && data.EntireObject);
if (needVar)
{
CodeVariableReferenceExpression var = new(name);
SetExpression(manager, c, var);
if (!shimObjectNames.Contains(name))
{
shimObjectNames.Add(name);
}
if (c.Site.TryGetService(out INestedContainer? nested) && nested.Components.Count > 0)
{
SetupVariableReferences(manager, nested, objectData, shimObjectNames);
}
}
}
}
}
/// <summary>
/// Serializes the given set of objects (contained in objectData) into the given object state dictionary.
/// </summary>
internal void Serialize(IDesignerSerializationManager manager, Dictionary<object, ObjectData> objectData, Dictionary<string, CodeDomComponentSerializationState> objectState, IList shimObjectNames)
{
if (manager.GetService<IContainer>() is { } container)
{
SetupVariableReferences(manager, container, objectData, shimObjectNames);
}
// Next, save a statement collection for each object.
StatementContext statementCtx = new();
statementCtx.StatementCollection.Populate(objectData.Keys);
manager.Context.Push(statementCtx);
try
{
foreach (ObjectData data in objectData.Values)
{
object? code = null;
CodeStatementCollection? ctxStatements = null;
Dictionary<string, object?>? resources = null;
CodeStatementCollection extraStatements = [];
manager.Context.Push(extraStatements);
if (manager.TryGetSerializer(data._value.GetType(), out CodeDomSerializer? serializer))
{
if (data.EntireObject)
{
if (!IsSerialized(manager, data._value))
{
code = data.Absolute
? serializer.SerializeAbsolute(manager, data._value)
: serializer.Serialize(manager, data._value);
ctxStatements = statementCtx.StatementCollection[data._value];
if (ctxStatements?.Count == 0)
{
ctxStatements = null;
}
if (extraStatements.Count > 0 && code is CodeStatementCollection existingStatements)
{
existingStatements.AddRange(extraStatements);
}
}
else
{
code = statementCtx.StatementCollection[data._value];
}
}
else
{
CodeStatementCollection codeStatements = [];
foreach (MemberData md in data.Members)
{
if (md._member.Attributes.Contains(DesignOnlyAttribute.Yes))
{
// For design time properties, we write their value into a resource blob.
#pragma warning disable SYSLIB0050 // Type or member is obsolete
if (md._member is PropertyDescriptor prop && prop.PropertyType.IsSerializable)
{
resources ??= [];
resources[prop.Name] = prop.GetValue(data._value);
}
#pragma warning restore SYSLIB0050
}
else
{
codeStatements.AddRange(md._absolute
? serializer.SerializeMemberAbsolute(manager, data._value, md._member)
: serializer.SerializeMember(manager, data._value, md._member));
}
}
code = codeStatements;
}
}
if (extraStatements.Count > 0)
{
if (code is CodeStatementCollection existingStatements)
{
existingStatements.AddRange(extraStatements);
}
}
manager.Context.Pop();
// And now search for default properties and events
List<string>? defaultPropList = null;
List<string>? defaultEventList = null;
IEventBindingService? ebs = manager.GetService<IEventBindingService>();
if (data.EntireObject)
{
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(data._value);
foreach (PropertyDescriptor prop in props)
{
if (!prop.ShouldSerializeValue(data._value)
&& !prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Hidden))
{
if (!prop.IsReadOnly || prop.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content))
{
defaultPropList ??= new(data.Members.Count);
defaultPropList.Add(prop.Name);
}
}
}
if (ebs is not null)
{
PropertyDescriptorCollection events = ebs.GetEventProperties(TypeDescriptor.GetEvents(data._value));
foreach (PropertyDescriptor eventProp in events)
{
if (eventProp is null || eventProp.IsReadOnly)
{
continue;
}
if (eventProp.GetValue(data._value) is null)
{
defaultEventList ??= [];
defaultEventList.Add(eventProp.Name);
}
}
}
}
else
{
foreach (MemberData md in data.Members)
{
if (md._member is PropertyDescriptor prop && !prop.ShouldSerializeValue(data._value))
{
if (ebs?.GetEvent(prop) is not null)
{
Debug.Assert(prop.GetValue(data._value) is null, "ShouldSerializeValue and GetValue are differing");
defaultEventList ??= [];
defaultEventList.Add(prop.Name);
}
else
{
defaultPropList ??= new(data.Members.Count);
defaultPropList.Add(prop.Name);
}
}
}
}
// Check for non-default modifiers property
object? modifier = TypeDescriptor.GetProperties(data._value)["Modifiers"]?.GetValue(data._value);
if (code is not null || defaultPropList is not null)
{
objectState[data._name] = new CodeDomComponentSerializationState(code, ctxStatements, defaultPropList, resources, defaultEventList, modifier);
}
}
}
finally
{
manager.Context.Pop();
}
}
}
}
}