// 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; using System.Globalization; using System.Resources; using System.Runtime.Serialization; namespace System.ComponentModel.Design.Serialization; internal partial class ResourceCodeDomSerializer { /// <summary> /// This is the meat of resource serialization. This implements a resource manager through a host-provided /// <see cref="IResourceService"/> interface. The resource service feeds us with resource readers and writers, /// and we simulate a runtime ResourceManager. There is one instance of this object for the entire /// serialization process, just like there is one resource manager in runtime code. When an instance /// of this object is created, it adds itself to the serialization manager's service list, /// and listens for the SerializationComplete event. When serialization is complete, this will close and /// flush any readers or writers it may have opened and will also remove itself from the service list. /// </summary> internal class SerializationResourceManager : ComponentResourceManager { private readonly IDesignerSerializationManager _manager; private bool _checkedLocalizationLanguage; private CultureInfo? _localizationLanguage; private IResourceWriter? _writer; private CultureInfo? _readCulture; private readonly Dictionary<string, int> _nameTable; private Dictionary<CultureInfo, Dictionary<string, object?>?>? _resourceSets; private Dictionary<string, object?>? _metadata; private Dictionary<string, object?>? _mergedMetadata; private object? _rootComponent; private HashSet<object>? _propertyFillAdded; private bool _invariantCultureResourcesDirty; private bool _metadataResourcesDirty; public SerializationResourceManager(IDesignerSerializationManager manager) { _manager = manager; _nameTable = []; // We need to know when we're done so we can push the resource file out. manager.SerializationComplete += OnSerializationComplete; } /// <summary> /// State the serializers use to determine if the declaration of this resource manager has been performed. This /// is just per-document state we keep; we do not actually care about this value. /// </summary> public bool DeclarationAdded { get; set; } /// <summary> /// When a declaration is added, we also setup an expression other serializers can use to reference our resource /// declaration. This bit tracks if we have setup this expression yet. Note that the expression and declaration /// may be added at different times, if the declaration was added by a cached component. /// </summary> public bool ExpressionAdded { get; set; } /// <summary> /// The language we should be localizing into. /// </summary> private CultureInfo? LocalizationLanguage { get { if (_checkedLocalizationLanguage) { return _localizationLanguage; } // Check to see if our base component's localizable prop is true if (_manager.TryGetContext(out RootContext? rootCtx)) { object comp = rootCtx.Value; if (TypeDescriptorHelper.TryGetPropertyValue(comp, "LoadLanguage", out CultureInfo? cultureInfo)) { _localizationLanguage = cultureInfo; } } _checkedLocalizationLanguage = true; return _localizationLanguage; } } /// <summary> /// This is the culture info we should use to read and write resources. We always write using the same /// culture we read with so we don't stomp on data. /// </summary> private CultureInfo ReadCulture => _readCulture ??= LocalizationLanguage ?? CultureInfo.InvariantCulture; /// <summary> /// Returns a hash table where we shove resource sets. /// </summary> private Dictionary<CultureInfo, Dictionary<string, object?>?> ResourceTable => _resourceSets ??= []; /// <summary> /// Retrieves the root component we're designing. /// </summary> private object? RootComponent => _rootComponent ??= _manager.GetContext<RootContext>()?.Value; /// <summary> /// Retrieves a resource writer we should write into. /// </summary> private IResourceWriter Writer { get { if (_writer is null) { if (_manager.TryGetService(out IResourceService? rs)) { // We always write with the culture we read with. In the event of a language change // during localization, we will write the new language to the source code // and then perform a reload. _writer = rs.GetResourceWriter(ReadCulture); } else { // No resource service, so there is no way to create a resource writer for the object. // In this case we just create an empty one so the resources go into the bit-bucket. Debug.Fail("We expected to get IResourceService -- no resource serialization will be available"); _writer = new ResourceWriter(new MemoryStream()); } } return _writer; } } /// <summary> /// The component serializer supports caching serialized outputs for speed. It holds both a collection /// of statements as well as an opaque blob for resources. This function adds data to that blob. /// The parameters to this function are the same as those to SetValue, /// or SetMetadata (when isMetadata is <see langword="true"/>). /// </summary> private static void AddCacheEntry(IDesignerSerializationManager manager, string name, object? value, bool isMetadata, bool forceInvariant, bool shouldSerializeValue, bool ensureInvariant) { if (manager.TryGetContext(out ComponentCache.Entry? entry)) { ComponentCache.ResourceEntry re = new( name, value, forceInvariant, shouldSerializeValue, ensureInvariant, manager.GetContext<PropertyDescriptor>(), manager.GetContext<ExpressionContext>()); if (isMetadata) { entry.AddMetadata(re); } else { if (re.PropertyDescriptor is null) { throw new ArgumentException("Serialization manager should have a PropertyDescriptor context"); } if (re.ExpressionContext is null) { throw new ArgumentException("Serialization manager should have an ExpressionContext context"); } entry.AddResource(re); } } } /// <summary> /// Returns true if the caller should add a property fill statement for the given object. /// A property fill is required for the component only once, so this remembers the value. /// </summary> public bool AddPropertyFill(object value) { _propertyFillAdded ??= []; return _propertyFillAdded.Add(value); } /// <summary> /// This method examines all the resources for the provided culture. /// When it finds a resource with a key in the format of "[objectName].[property name]"; /// it will apply that resources value to the corresponding property on the object. /// </summary> [RequiresUnreferencedCode("The Type of value cannot be statically discovered.")] public override void ApplyResources(object value, string objectName, CultureInfo? culture) { culture ??= ReadCulture; // .NET Framework 4.0 (Dev10 #425129): Control location moves due to incorrect anchor info when resource files are reloaded. Windows.Forms.Control? control = value as Windows.Forms.Control; control?.SuspendLayout(); base.ApplyResources(value, objectName, culture); control?.ResumeLayout(false); } /// <summary> /// This determines if the given resource name/value pair can be retrieved from a parent culture. /// We don't want to write duplicate resources for each language, so we do a check of the parent culture. /// </summary> private CompareValue CompareWithParentValue(string name, object? value) { Debug.Assert(name is not null, "name is null"); // If there is no parent culture, treat that as being different from the parent's resource. // which results in the "normal" code path for the caller. return ReadCulture.Equals(CultureInfo.InvariantCulture) ? CompareValue.Different : CompareWithParentValue(ReadCulture, name, value); } private CompareValue CompareWithParentValue(CultureInfo culture, string name, object? value) { Debug.Assert(culture.Parent != culture, "should have returned when culture = InvariantCulture"); CultureInfo parent = culture.Parent; Dictionary<string, object?>? resourceSet = GetResourceSet(culture); if (resourceSet is not null && resourceSet.TryGetValue(name, out object? parentValue)) { return parentValue is null || !parentValue.Equals(value) ? CompareValue.Different : CompareValue.Same; } else if (culture.Equals(CultureInfo.InvariantCulture)) { return CompareValue.New; } return CompareWithParentValue(parent, name, value); } /// <summary> /// Creates a resource set dictionary for the given resource reader. /// </summary> private Dictionary<string, object?> CreateResourceSet(IResourceReader reader, CultureInfo culture) { Dictionary<string, object?> result = []; // We need to guard against bad or unloadable resources. We warn the user in the task list here, // but we will still load the designer. try { IDictionaryEnumerator resEnum = reader.GetEnumerator(); while (resEnum.MoveNext()) { string name = (string)resEnum.Key; object? value = resEnum.Value; result[name] = value; } } catch (Exception e) { string message = e.Message; if (string.IsNullOrEmpty(message)) { message = e.GetType().Name; } Exception se = culture == CultureInfo.InvariantCulture ? new SerializationException(string.Format(SR.SerializerResourceExceptionInvariant, message), e) : (Exception)new SerializationException(string.Format(SR.SerializerResourceException, culture.ToString(), message), e); _manager.ReportError(se); } return result; } /// <summary> /// This returns a dictionary enumerator for metadata on the invariant culture. /// If no metadata can be found this will return null. /// </summary> public IDictionaryEnumerator? GetMetadataEnumerator() { if (_mergedMetadata is not null) { return _mergedMetadata.GetEnumerator(); } Dictionary<string, object?>? metaData = GetMetadata(); if (metaData is not null) { // This is for backwards compatibility and also for the case when our reader/writer don't // support metadata. We must merge the original enumeration data in here or else existing // design time properties won't show up. That would be really bad for things like Localizable. Dictionary<string, object?>? resourceSet = GetResourceSet(CultureInfo.InvariantCulture); if (resourceSet is not null) { foreach (KeyValuePair<string, object?> item in resourceSet) { metaData.TryAdd(item.Key, item.Value); } } _mergedMetadata = metaData; } return _mergedMetadata?.GetEnumerator(); } /// <summary> /// This returns a dictionary enumerator for the given culture. /// If no such resource file exists for the culture this will return null. /// </summary> public IDictionaryEnumerator? GetEnumerator(CultureInfo culture) { Dictionary<string, object?>? ht = GetResourceSet(culture); return ht?.GetEnumerator(); } /// <summary> /// Loads the metadata table /// </summary> private Dictionary<string, object?>? GetMetadata() { if (_metadata is not null) { return _metadata; } IResourceService? resSvc = _manager.GetService<IResourceService>(); IResourceReader? reader = resSvc?.GetResourceReader(CultureInfo.InvariantCulture); if (reader is not null) { try { if (reader is ResXResourceReader resxReader) { _metadata = []; IDictionaryEnumerator de = resxReader.GetMetadataEnumerator(); while (de.MoveNext()) { _metadata[(string)de.Key] = de.Value; } } } finally { reader.Close(); } } return _metadata; } /// <summary> /// Overrides ResourceManager.GetObject to return the requested object. Returns null if the object couldn't be found. /// </summary> public override object? GetObject(string resourceName) { return GetObject(resourceName, false); } /// <summary> /// Retrieves the object of the given name from our resource bundle. /// If forceInvariant is <see langword="true"/>, this will always use the invariant resource, /// rather than using the current language. /// Returns <see langword="null"/> if the object couldn't be found. /// </summary> public object? GetObject(string resourceName, bool forceInvariant) { Debug.Assert(_manager is not null, "This resource manager object has been destroyed."); // We fetch the read culture if someone asks for a culture-sensitive string. // If forceInvariant is set, we always use the invariant culture. CultureInfo culture = forceInvariant ? CultureInfo.InvariantCulture : ReadCulture; CultureInfo lastCulture; object? value = null; do { Dictionary<string, object?>? rs = GetResourceSet(culture); rs?.TryGetValue(resourceName, out value); lastCulture = culture; culture = culture.Parent; } while (value is null && !lastCulture.Equals(culture)); return value; } /// <summary> /// Looks up the resource set in the resourceSets hash table, loading the set if it hasn't been loaded already. /// Returns <see langword="null"/> if no resource that exists for that culture. /// </summary> private Dictionary<string, object?>? GetResourceSet(CultureInfo culture) { Debug.Assert(culture is not null, "null parameter"); if (!ResourceTable.TryGetValue(culture, out Dictionary<string, object?>? resourceSet)) { if (_manager.TryGetService(out IResourceService? resSvc)) { IResourceReader? reader = resSvc.GetResourceReader(culture); if (reader is not null) { try { resourceSet = CreateResourceSet(reader, culture); } finally { reader.Close(); } } else if (culture.Equals(CultureInfo.InvariantCulture)) { // If this is the invariant culture, always provide a resource set. resourceSet = []; } // resourceSet may be null here. We add it to the cache anyway as a sentinel so we don't repeatedly ask for the same resource. ResourceTable[culture] = resourceSet; } } return resourceSet; } /// <summary> /// Override of GetResourceSet from ResourceManager. /// </summary> public override ResourceSet? GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { ArgumentNullException.ThrowIfNull(culture); CultureInfo lastCulture; do { lastCulture = culture; culture = culture.Parent; } while (tryParents && !lastCulture.Equals(culture)); return createIfNotExists ? new CodeDomResourceSet() : null; } /// <summary> /// Overrides ResourceManager.GetString to return the requested string. Returns null if the string couldn't be found. /// </summary> public override string? GetString(string resourceName) { return GetObject(resourceName, false) as string; } /// <summary> /// Event handler that gets called when serialization or deserialization is complete. /// Here we need to write any resources to disk. Since we open resources for write on demand, /// this code handles the case of reading resources as well. /// </summary> private void OnSerializationComplete(object? sender, EventArgs e) { // Commit any changes we have made. _writer?.Close(); _writer = null; if (_invariantCultureResourcesDirty || _metadataResourcesDirty) { if (_manager.TryGetService(out IResourceService? service)) { IResourceWriter invariantWriter = service.GetResourceWriter(CultureInfo.InvariantCulture); Debug.Assert(invariantWriter is not null, "GetResourceWriter returned null for the InvariantCulture"); try { // Do the invariant resources first Debug.Assert(!ReadCulture.Equals(CultureInfo.InvariantCulture), "invariantCultureResourcesDirty should only come into play when readCulture != CultureInfo.InvariantCulture; check that CompareWithParentValue is correct"); ResourceTable.TryGetValue(CultureInfo.InvariantCulture, out Dictionary<string, object?>? resourceSet); Debug.Assert(resourceSet is not null, "ResourceSet for the InvariantCulture not loaded, but it's considered dirty?"); // Dump the hash table to the resource writer foreach ((string name, object? value) in resourceSet) { invariantWriter.AddResource(name, value); } _invariantCultureResourcesDirty = false; // Followed by the metadata. Debug.Assert(_metadata is not null, "No metadata, but it's dirty?"); if (invariantWriter is ResXResourceWriter resxWriter) { foreach (KeyValuePair<string, object?> de in _metadata) { resxWriter.AddMetadata(de.Key, de.Value); } } else { Debug.Fail("Metadata not supported, but it's dirty?"); } _metadataResourcesDirty = false; } finally { invariantWriter.Close(); } } else { Debug.Fail("Couldn't find IResourceService"); _invariantCultureResourcesDirty = false; _metadataResourcesDirty = false; } } } /// <summary> /// Writes a metadata tag to the resource, or writes a normal tag if the resource writer doesn't support metadata. /// </summary> public void SetMetadata(IDesignerSerializationManager manager, string resourceName, object? value, bool shouldSerializeValue, bool applyingCachedResources) { #pragma warning disable SYSLIB0050 // Type or member is obsolete if (value is not null && (!value.GetType().IsSerializable)) { Debug.Fail($"Cannot save a non-serializable value into resources. Add serializable to {(value is null ? "(null)" : value.GetType().Name)}"); return; } #pragma warning restore SYSLIB0050 // If we are currently the invariant culture then we may be able to write directly. if (ReadCulture.Equals(CultureInfo.InvariantCulture)) { if (shouldSerializeValue) { if (Writer is ResXResourceWriter resxWriter) { resxWriter.AddMetadata(resourceName, value); } else { Writer.AddResource(resourceName, value); } } } else { // Check if the invariant writer supports metadata. If not, we need to push metadata as regular data. IResourceService? service = manager.GetService<IResourceService>(); IResourceWriter? invariantWriter = service?.GetResourceWriter(CultureInfo.InvariantCulture); // GetResourceSet never returns null for CultureInfo.InvariantCulture Dictionary<string, object?> invariant = GetResourceSet(CultureInfo.InvariantCulture)!; Dictionary<string, object?>? metadata; if (invariantWriter is null or ResXResourceWriter) { metadata = GetMetadata(); if (metadata is null) { _metadata = []; metadata = _metadata; } // Note that when we read metadata, for backwards compatibility, // we also merge in regular data from the invariant resource. // We need to clear that data here, since we are going to write out metadata separately. invariant.Remove(resourceName); _metadataResourcesDirty = true; } else { metadata = invariant; _invariantCultureResourcesDirty = true; } Debug.Assert(metadata is not null, "Don't know where to push metadata."); if (metadata is not null) { if (shouldSerializeValue) { metadata[resourceName] = value; } else { metadata.Remove(resourceName); } } _mergedMetadata = null; } // Update the component cache, if we have one active if (!applyingCachedResources) { AddCacheEntry(manager, resourceName, value, true, false, shouldSerializeValue, false); } } /// <summary> /// Writes the given resource value under the given name. This checks the parent resource to see if the values /// are the same. If they are, the resource is not written. If not, then the resource is written. /// We always write using the resource language we read in with, so we don't stomp on the wrong resource data /// in the event that someone changes the language. /// </summary> public void SetValue(IDesignerSerializationManager manager, string resourceName, object? value, bool forceInvariant, bool shouldSerializeInvariant, bool ensureInvariant, bool applyingCachedResources) { // Values we are going to serialize must be serializable or else the resource writer will fail when we close it. #pragma warning disable SYSLIB0050 // Type or member is obsolete if (value is not null && (!value.GetType().IsSerializable)) { Debug.Fail($"Cannot save a non-serializable value into resources. Add serializable to {(value is null ? "(null)" : value.GetType().Name)}"); return; } #pragma warning restore SYSLIB0050 if (forceInvariant) { if (ReadCulture.Equals(CultureInfo.InvariantCulture)) { if (shouldSerializeInvariant) { Writer.AddResource(resourceName, value); } } else { Dictionary<string, object?>? resourceSet = GetResourceSet(CultureInfo.InvariantCulture); Debug.Assert(resourceSet is not null, "No ResourceSet for the InvariantCulture?"); if (shouldSerializeInvariant) { resourceSet[resourceName] = value; } else { resourceSet.Remove(resourceName); } _invariantCultureResourcesDirty = true; } } else { CompareValue comparison = CompareWithParentValue(resourceName, value); switch (comparison) { case CompareValue.Same: // don't add to any resource set break; case CompareValue.Different: Writer.AddResource(resourceName, value); break; case CompareValue.New: if (ensureInvariant) { // Add resource to InvariantCulture Debug.Assert(!ReadCulture.Equals(CultureInfo.InvariantCulture), "invariantCultureResourcesDirty should only come into play when readCulture != CultureInfo.InvariantCulture; check that CompareWithParentValue is correct"); Dictionary<string, object?>? resourceSet = GetResourceSet(CultureInfo.InvariantCulture); Debug.Assert(resourceSet is not null, "No ResourceSet for the InvariantCulture?"); resourceSet[resourceName] = value; _invariantCultureResourcesDirty = true; Writer.AddResource(resourceName, value); } else { // This is a new value. We want to write it out, PROVIDED that the value is not associated // with a property that is currently returning false from ShouldSerializeValue. // This allows us to skip writing out Font == NULL on all non-invariant cultures, // but still allow us to write out the value if the user is resetting a font back to null. // If we cannot associate the value with a property we will write it out just to be safe. // In addition, we need to handle the case of the user adding a new component // to the non-invariant language. This would be bad, because when he/she moved back // to the invariant language the component's properties would all be defaults. // In order to minimize this problem, but still allow holes in the invariant resx, // we also check to see if the property can be reset. If it cannot be reset, // that means that it has no meaningful default. Therefore, it should have appeared // in the invariant resx and its absence indicates a new component. bool writeValue = true; bool writeInvariant = false; if (manager.TryGetContext(out PropertyDescriptor? prop)) { if (manager.TryGetContext(out ExpressionContext? tree) && tree.Expression is CodePropertyReferenceExpression) { writeValue = prop.ShouldSerializeValue(tree.Owner); writeInvariant = !prop.CanResetValue(tree.Owner); } } if (writeValue) { Writer.AddResource(resourceName, value); if (writeInvariant) { // Add resource to InvariantCulture Debug.Assert(!ReadCulture.Equals(CultureInfo.InvariantCulture), "invariantCultureResourcesDirty should only come into play when readCulture != CultureInfo.InvariantCulture; check that CompareWithParentValue is correct"); Dictionary<string, object?>? resourceSet = GetResourceSet(CultureInfo.InvariantCulture); Debug.Assert(resourceSet is not null, "No ResourceSet for the InvariantCulture?"); resourceSet[resourceName] = value; _invariantCultureResourcesDirty = true; } } } break; default: Debug.Fail($"Unknown CompareValue {comparison}"); break; } } // Update the component cache, if we have one active. We don't have to be fancy here because updating // this cache just indicates that code in the component cache will later call us to re-apply the resources, // and our logic above will be called again. if (!applyingCachedResources) { AddCacheEntry(manager, resourceName, value, false, forceInvariant, shouldSerializeInvariant, ensureInvariant); } } /// <summary> /// Writes the given resource value under the given name. /// This checks the parent resource to see if the values are the same. /// If they are, the resource is not written. If not, then the resource is written. /// We always write using the resource language we read in with, /// so we don't stomp on the wrong resource data in the event that someone changes the language. /// </summary> public string SetValue(IDesignerSerializationManager manager, ExpressionContext? tree, object? value, bool forceInvariant, bool shouldSerializeInvariant, bool ensureInvariant, bool applyingCachedResources) { string? nameBase; bool appendCount = false; if (tree is not null) { if (tree.Owner == RootComponent) { nameBase = "$this"; } else { nameBase = manager.GetName(tree.Owner); if (nameBase is null && manager.TryGetService(out IReferenceService? referenceService)) { nameBase = referenceService.GetName(tree.Owner); } } CodeExpression expression = tree.Expression; string? expressionName; if (expression is CodePropertyReferenceExpression codeProperty) { expressionName = codeProperty.PropertyName; } else if (expression is CodeFieldReferenceExpression codeField) { expressionName = codeField.FieldName; } else if (expression is CodeMethodReferenceExpression codeMethod) { expressionName = codeMethod.MethodName; if (expressionName.StartsWith("Set", StringComparison.InvariantCulture)) { expressionName = expressionName[3..]; } } else { expressionName = null; } nameBase ??= "resource"; if (expressionName is not null) { nameBase += "." + expressionName; } } else { nameBase = "resource"; appendCount = true; } // Now find an unused name string resourceName = nameBase; // Only append the number when appendCount is set or if there is already a count. int count = 0; if (appendCount || _nameTable.ContainsKey(nameBase)) { _nameTable.TryGetValue(nameBase, out count); do { count++; resourceName = $"{nameBase}{count}"; } while (_nameTable.ContainsKey(resourceName)); } // Now that we have a name, write out the resource. SetValue(manager, resourceName, value, forceInvariant, shouldSerializeInvariant, ensureInvariant, applyingCachedResources); _nameTable[resourceName] = count; return resourceName; } private class CodeDomResourceSet : ResourceSet { public CodeDomResourceSet() { } } private enum CompareValue { Same, // parent value == child value Different, // parent value exists, but != child value New, // parent value does not exist } } } |