|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// Contents: XAML writer
//
using System.ComponentModel;
using System.Reflection;
using System.Collections;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace System.Windows.Markup.Primitives
{
/// <summary>
/// Routines to write a tree of objects to XAML format
/// </summary>
public sealed class MarkupWriter : IDisposable
{
/// <summary>
/// Create an instance of a MarkupObject for the given object.
/// </summary>
/// <param name="instance">
/// The instance of an object that will be treated as the root of a serialization tree
/// </param>
/// <returns>
/// A MarkupObject that allows iterating a tree of objects as a serializer
/// </returns>
public static MarkupObject GetMarkupObjectFor(object instance)
{
ArgumentNullException.ThrowIfNull(instance);
XamlDesignerSerializationManager manager = new XamlDesignerSerializationManager(null);
manager.XamlWriterMode = XamlWriterMode.Expression;
return new ElementMarkupObject(instance, manager);
}
/// <summary>
/// Create an instance of a MarkupObject for the given object.
/// </summary>
/// <param name="instance">
/// The instance of an object that will be treated as the root of a serialization tree
/// </param>
/// <param name="manager">
/// The serialization manager to use.
/// </param>
/// <returns>
/// A MarkupObject that allows iterating a tree of objects as a serializer
/// </returns>
public static MarkupObject GetMarkupObjectFor(object instance, XamlDesignerSerializationManager manager)
{
ArgumentNullException.ThrowIfNull(instance);
ArgumentNullException.ThrowIfNull(manager);
return new ElementMarkupObject(instance, manager);
}
/// <summary>
/// Serialize the given instance to a XML writer.
/// </summary>
/// <param name="writer">
/// The writer to use to write the serialized image of the instance
/// </param>
/// <param name="instance">
/// The object instance to serialize
/// </param>
internal static void SaveAsXml(XmlWriter writer, object instance)
{
ArgumentNullException.ThrowIfNull(writer);
SaveAsXml(writer, GetMarkupObjectFor(instance));
}
/// <summary>
/// Serialize the given instance to a XML writer.
/// </summary>
/// <param name="writer">
/// The writer to use to write the serialized image of the instance
/// </param>
/// <param name="manager">
/// The serialization manager to use.
/// </param>
/// <param name="instance">
/// The object instance to serialize
/// </param>
internal static void SaveAsXml(XmlWriter writer, object instance, XamlDesignerSerializationManager manager)
{
ArgumentNullException.ThrowIfNull(writer);
ArgumentNullException.ThrowIfNull(manager);
manager.ClearXmlWriter();
SaveAsXml(writer, GetMarkupObjectFor(instance, manager));
}
/// <summary>
/// Serialize the given item to an XML writer.
/// </summary>
/// <param name="writer">
/// The writer to use to writer the serialized image of the item
/// </param>
/// <param name="item">
/// The item to serialize. For example, the return value of GetMarkupObjectFor
/// </param>
internal static void SaveAsXml(XmlWriter writer, MarkupObject item)
{
// Consider turning Debug.Assert's in the WriteItem into exceptions
// if this method is public.
ArgumentNullException.ThrowIfNull(writer);
ArgumentNullException.ThrowIfNull(item);
try
{
using (MarkupWriter markupWriter = new MarkupWriter(writer))
{
markupWriter.WriteItem(item);
}
}
finally
{
writer.Flush();
}
}
/// <summary>
/// Throws an exception if the given type cannot be serialized because it is
/// 1) not public
/// 2) a nested-public
/// 3) a generic type
/// </summary>
/// <param name="type">
/// The type to be checked
/// </param>
internal static void VerifyTypeIsSerializable(Type type)
{
// Check the type to make sure that it is not a nested type, that it is public, and that it is not generic
if (type.IsNestedPublic)
{
throw new InvalidOperationException( SR.Format( SR.MarkupWriter_CannotSerializeNestedPublictype, type.ToString() ));
}
if (!type.IsPublic )
{
throw new InvalidOperationException( SR.Format( SR.MarkupWriter_CannotSerializeNonPublictype, type.ToString() ));
}
if (type.IsGenericType)
{
throw new InvalidOperationException( SR.Format( SR.MarkupWriter_CannotSerializeGenerictype, type.ToString() ));
}
}
#region Internal Implementation
internal MarkupWriter(XmlWriter writer)
{
_writer = writer;
_xmlTextWriter = writer as XmlTextWriter;
}
/// <summary>
/// Dispose method
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
}
private bool RecordNamespaces(Scope scope, MarkupObject item, IValueSerializerContext context, bool
lastWasString)
{
// Ensure that there's an xmlns declaration generated for strings that are emitted as content.
// For example, <Button>Hello</Button> should not bring in the System namespace from mscorlib via xmlns but,
// <Button><s:String>Hello</s:String><Button> should.
bool result = true;
if (lastWasString || item.ObjectType != typeof(string) || HasNonValueProperties(item))
{
scope.MakeAddressable(item.ObjectType);
result = false;
}
item.AssignRootContext(context);
lastWasString = false;
foreach (MarkupProperty property in item.Properties)
{
if (property.IsComposite)
{
bool isCollection = IsCollectionType(property.PropertyType);
foreach (MarkupObject subItem in property.Items)
lastWasString = RecordNamespaces(scope, subItem, context, lastWasString || isCollection);
}
else
scope.MakeAddressable(property.TypeReferences);
if (property.DependencyProperty != null)
scope.MakeAddressable(property.DependencyProperty.OwnerType);
if (property.IsKey)
scope.MakeAddressable(NamespaceCache.XamlNamespace);
}
return result;
}
const string clrUriPrefix = "clr-namespace:";
/// <summary>
/// Partially ordered lists. Elements are stored in order
/// that obeys the SetOrder calls. Elements need only be
/// partially ordered instead of fully ordered. A full ordering
/// is assigned that obeys the partial ordering declared.
///
/// Limitations:
///
/// Cyclic references
/// -----------------
/// This does not detect cyclic SetOrder values,
/// for example, using string keys,
///
/// SetOrder("a", "b");
/// SetOrder("b", "c");
/// SetOrder("c", "a");
///
/// No full ordering can obey this partial ordering. This is
/// not detected and the first partial ordering (the oldest)
/// is ignored.
///
/// Null values
/// -----------
/// This list cannot contain null (it is used a sentinel value)
/// but this can be fixed if needed by creating a not-present value
/// using new Object() but since I didn't need it I didn't implement
/// it.
/// </summary>
/// <typeparam name="TKey">The type of the key to use for the partial ordering</typeparam>
/// <typeparam name="TValue">The type of the elements in the list</typeparam>
private class PartiallyOrderedList<TKey, TValue> : IEnumerable<TValue>
where TValue : class
{
/// <summary>
/// Private class to hold the key and the value.
/// </summary>
private class Entry
{
public readonly TKey Key;
public readonly TValue Value;
public List<int> Predecessors;
public int Link; // -1 - unseen; <-1 - in DFS; >=0 - next index in top. order
public const int UNSEEN = -1;
public const int INDFS = -2;
public Entry(TKey key, TValue value)
{
Debug.Assert( (object)key != null);
Key = key;
Value = value;
Predecessors = null;
Link = 0;
}
public override bool Equals(object obj)
{
Entry other = obj as Entry;
return other != null && other.Key.Equals(Key);
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
}
/// <summary>
/// Add a value to the list. The order in the list
/// is controled by the key and calls to SetOrder.
/// </summary>
/// <param name="key">The ordering key</param>
/// <param name="value">The value</param>
public void Add(TKey key, TValue value)
{
Entry entry = new Entry(key, value);
int existingIndex = _entries.IndexOf(entry);
// If an entry already exists use it. This happens
// when two values of the same key are added or when
// SetOrder refering to a key that hasn't had a value
// added for it yet. A null value is used as a place
// holder.
if (existingIndex >= 0)
{
entry.Predecessors = _entries[existingIndex].Predecessors;
_entries[existingIndex] = entry;
}
else
{
_entries.Add(entry);
}
}
private int GetEntryIndex(TKey key)
{
Entry entry = new Entry(key, null);
int result = _entries.IndexOf(entry);
if (result < 0)
{
result = _entries.Count;
_entries.Add(entry);
}
return result;
}
public void SetOrder(TKey predecessor, TKey key)
{
// Find where these keys are in the list
// If they don't exist, a null value place holder is
// added to the end of the list.
int predIndex = GetEntryIndex(predecessor);
Entry predEntry = _entries[predIndex];
int keyIndex = GetEntryIndex(key);
Entry keyEntry = _entries[keyIndex];
// add the constraint
if (keyEntry.Predecessors == null)
{
keyEntry.Predecessors = new List<int>();
}
keyEntry.Predecessors.Add(predIndex);
// mark the list to force a sort before the next
// enumeration
_firstIndex = Entry.UNSEEN;
}
// compute a linear order consistent with the constraints.
// This is the classic Topological Sort problem, which is
// solved in linear time by doing a depth-first search of the
// "reverse" directed graph (edges go from a node to its
// predecessors), and enumerating the nodes in postorder.
// If there are no cycles, each node is enumerated after any
// nodes that directly or indirectly precede it.
private void TopologicalSort()
{
// initialize
_firstIndex = Entry.UNSEEN;
_lastIndex = Entry.UNSEEN;
for (int i=0; i<_entries.Count; ++i)
{
_entries[i].Link = Entry.UNSEEN;
}
// start a DFS at each entry
for (int i=0; i<_entries.Count; ++i)
{
DepthFirstSearch(i);
}
}
// depth-first-search of predecessors of entry at given index
private void DepthFirstSearch(int index)
{
// do a search, unless we've already seen this entry
if (_entries[index].Link == Entry.UNSEEN)
{
// mark entry as 'in progress'
_entries[index].Link = Entry.INDFS;
// search the predecessors
if (_entries[index].Predecessors != null)
{
foreach (int predIndex in _entries[index].Predecessors)
{
DepthFirstSearch(predIndex);
}
}
// Add the current entry to the postorder list. We do this
// by linking the previous (in postorder) entry to this one
if (_lastIndex == -1)
{
_firstIndex = index; // special case for head of list
}
else
{
_entries[_lastIndex].Link = index;
}
_lastIndex = index;
}
/* Note: if it is desired to detect cycles, this is the
place to do it.
else if (_entries[index].Link == Entry.INDFS)
{
// DFS has returned to an entry that is currently being
// searched. This happens if and only if there is a cycle.
// Report the cycle.
}
*/
}
public IEnumerator<TValue> GetEnumerator()
{
// if new constraints have arrived, sort the list
if (_firstIndex < 0)
{
TopologicalSort();
}
// Enumerate the values according to the topological order.
// We skip null values that
// are just place holders for keys for which the order
// was set but the value wasn't provided.
int index = _firstIndex;
while (index >= 0)
{
Entry entry = _entries[index];
if (entry.Value != null)
yield return entry.Value;
index = entry.Link;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
// Non-generic version of IEnumerator
foreach (TValue value in this)
yield return value;
}
private List<Entry> _entries = new List<Entry>();
private int _firstIndex = Entry.UNSEEN; // head of linear order
private int _lastIndex; // index of most recently assigned entry
}
private const int EXTENSIONLENGTH = 9; // the number of characters in the string "Extension"
internal void WriteItem(MarkupObject item)
{
Debug.Assert(item != null && _writer != null);
// We must check the type here, even though we check again in WriteScope(item, scope), because we
// will wrap the type in an ExtensionSimpliefMarkupObject, which is the type that will be checked
// from WriteScope(item, scope).
VerifyTypeIsSerializable(item.ObjectType);
Scope scope = new Scope(null);
scope.RecordMapping("", NamespaceCache.GetNamespaceUriFor(item.ObjectType));
// Do a pass to ensure all namespaces are visible at the top of the file.
RecordNamespaces(scope, item, new MarkupWriterContext(scope), false);
// Write out the simplified form of markup extensions
item = new ExtensionSimplifierMarkupObject(item, null);
WriteItem(item, scope);
_writer = null;
}
private void WriteItem(MarkupObject item, Scope scope)
{
VerifyTypeIsSerializable(item.ObjectType);
// Start a new scope for this item
MarkupWriterContext context = new MarkupWriterContext(scope);
item.AssignRootContext(context);
// Write the start of the element
string uri = scope.MakeAddressable(item.ObjectType);
string prefix = scope.GetPrefixOf(uri);
string name = item.ObjectType.Name;
if (typeof(MarkupExtension).IsAssignableFrom(item.ObjectType) &&
name.EndsWith("Extension", StringComparison.Ordinal))
{
// Prefer MarkupExtensions without the "Extension" postfix.
name = name.Substring(0, name.Length - EXTENSIONLENGTH);
}
_writer.WriteStartElement(prefix, name, uri);
// Write attributes
ContentPropertyAttribute cpa = item.Attributes[typeof(ContentPropertyAttribute)] as ContentPropertyAttribute;
XmlLangPropertyAttribute xlpa = item.Attributes[typeof(XmlLangPropertyAttribute)] as XmlLangPropertyAttribute;
UidPropertyAttribute upa = item.Attributes[typeof(UidPropertyAttribute)] as UidPropertyAttribute;
MarkupProperty contentProperty = null;
bool first = true;
int argumentCount = 0;
List<int> argumentCompositeIndexes = null;
bool noOtherPropertiesAllowed = false;
List<MarkupProperty> composites = null;
Dictionary<string, string> writtenAttributes = new Dictionary<string, string>();
PartiallyOrderedList<string, MarkupProperty> deferredProperties = null;
Formatting previousFormatting = (_xmlTextWriter != null) ? _xmlTextWriter.Formatting : Formatting.None;
foreach (MarkupProperty property in item.GetProperties(false /*mapToConstructorArgs*/))
{
if (property.IsConstructorArgument)
{
// When the reader supports <x:Argument1>...</x:Argument1> format, do the following:
// _writer.WriteStartElement(string.Format(CultureInfo.InvariantCulture, "Argument{0}", argumentCompositeIndexes[argumentCompositeIndex++]), NamespaceCache.XamlNamespace);
// The writer generates an exception for now:
throw new InvalidOperationException(SR.UnserializableKeyValue);
}
Debug.Assert(!noOtherPropertiesAllowed || property.IsKey,
"Problem with MarkupObject implemenation: Items returning a ValueAsString can have no other properties");
if (IsContentProperty(property, cpa, ref contentProperty))
{
first = false;
continue;
}
if (IsDeferredProperty(property, writtenAttributes, ref deferredProperties))
{
continue;
}
if (!property.IsComposite)
{
if (property.IsAttached || property.PropertyDescriptor == null)
{
if (property.IsValueAsString)
{
// "Property" represents the content passed to a type converter
Debug.Assert(first, "Problem with MarkupObject implemenation: Items returning a ValueAsString can have no other properties");
contentProperty = property;
noOtherPropertiesAllowed = true;
first = false;
continue;
}
if (property.IsKey)
{
// "Property" is the key to the this dictionary entry
scope.MakeAddressable(NamespaceCache.XamlNamespace);
_writer.WriteAttributeString(scope.GetPrefixOf(NamespaceCache.XamlNamespace), "Key", NamespaceCache.XamlNamespace, property.StringValue);
continue;
}
// Property is attached
DependencyProperty dependencyProperty = property.DependencyProperty;
Debug.Assert(dependencyProperty != null, "Problem with MarkupObject implementation: If PropertyDescriptor is null one of the following needs to be true; IsKey, IsValueAsString, IsConstructorArgument, or DependencyProperty is not null");
string typeUri = scope.MakeAddressable(dependencyProperty.OwnerType);
scope.MakeAddressable(property.TypeReferences);
if (property.Attributes[typeof(DesignerSerializationOptionsAttribute)] != null)
{
DesignerSerializationOptionsAttribute option = property.Attributes[typeof(DesignerSerializationOptionsAttribute)] as DesignerSerializationOptionsAttribute;
if (option.DesignerSerializationOptions == DesignerSerializationOptions.SerializeAsAttribute)
{
if (dependencyProperty == UIElement.UidProperty)
{
// Force UIElement.Uid to be serialized as x:Uid
string xamlUri = scope.MakeAddressable(typeof(TypeExtension));
_writer.WriteAttributeString(scope.GetPrefixOf(xamlUri), dependencyProperty.Name, xamlUri, property.StringValue);
}
continue;
}
}
property.VerifyOnlySerializableTypes();
string propertyPrefix = scope.GetPrefixOf(typeUri);
string localName = $"{dependencyProperty.OwnerType.Name}.{dependencyProperty.Name}";
if (string.IsNullOrEmpty(propertyPrefix))
{
_writer.WriteAttributeString(localName, property.StringValue);
}
else
{
_writer.WriteAttributeString(propertyPrefix, localName, typeUri, property.StringValue);
}
}
else
{
property.VerifyOnlySerializableTypes();
if (xlpa != null && xlpa.Name == property.PropertyDescriptor.Name)
{
// This is an xml:lang attribute
_writer.WriteAttributeString("xml", "lang", NamespaceCache.XmlNamespace, property.StringValue);
}
else if (upa != null && upa.Name == property.PropertyDescriptor.Name)
{
string xamlUri = scope.MakeAddressable(NamespaceCache.XamlNamespace);
_writer.WriteAttributeString(scope.GetPrefixOf(xamlUri), property.PropertyDescriptor.Name, xamlUri, property.StringValue);
}
else
{
_writer.WriteAttributeString(property.PropertyDescriptor.Name, property.StringValue);
}
writtenAttributes[property.Name] = property.Name;
}
}
else
{
if (property.DependencyProperty != null)
{
// ensure xmlns are not needed on attached composite properties.
scope.MakeAddressable(property.DependencyProperty.OwnerType);
}
if (property.IsKey)
{
// "Property" is the key to the this dictionary entry
scope.MakeAddressable(NamespaceCache.XamlNamespace);
}
else if (property.IsConstructorArgument)
{
scope.MakeAddressable(NamespaceCache.XamlNamespace);
if (argumentCompositeIndexes == null)
{
argumentCompositeIndexes = new List<int>();
}
argumentCompositeIndexes.Add(++argumentCount);
}
if (composites == null)
{
composites = new List<MarkupProperty>();
}
composites.Add(property);
}
}
foreach (Mapping mapping in scope.EnumerateLocalMappings)
{
// New mappings
_writer.WriteAttributeString("xmlns", mapping.Prefix, NamespaceCache.XmlnsNamespace, mapping.Uri);
}
// Before we are finished, check to see if we need to emit an xml:space="preserve" attribute
// by detecting if we are writing any raw strings out and if they will be changed due to
// normalization. This is done by checking if the normalized version of the string would differ
// from the non-normalized version.
if (!scope.XmlnsSpacePreserve && contentProperty != null &&
!HasOnlyNormalizationNeutralStrings(contentProperty, false, false))
{
_writer.WriteAttributeString("xml", "space", NamespaceCache.XmlNamespace, "preserve");
scope.XmlnsSpacePreserve = true;
// Per the documentation for XmlWriterSettings.Indent, elements are indented as
// long as the element does not contain mixed content. Once WriteString or
// WriteWhiteSpace method is called to write out a mixed element content,
// the XmlWriter stops indenting. The indenting resumes once the mixed content
// element is closed.
//
// It is desirable to ensure that indentation is suspended within
// an element with xml:space="preserve". Here, we make a dummy call to WriteString
// to indicate to the XmlWriter that we are about to write a "mixed" element
// content. When we call WriteEndElement later in this method, indentation
// behavior will be rolled back to that of the parent element (typically,
// indentaiton will simply be resumed).
//
// If the underlying XmlWriterSettings did not specify indentation, this would
// have no net effect.
_writer.WriteString(string.Empty);
if( scope.IsTopOfSpacePreservationScope && _xmlTextWriter != null )
{
// If we are entering a xml:space="preserve" scope and using
// a XmlTextWriter, we need to turn off its formatting options.
// Otherwise, XmlTextWriter will continue injecting extraneous
// whitespace characters.
_xmlTextWriter.Formatting = Formatting.None;
}
}
// See the comment in property.IsConstructorArgument
// int argumentCompositeIndex = 0;
// Write composite properties
if (composites != null)
{
foreach (MarkupProperty property in composites)
{
bool propertyTagWritten = false;
bool explicitTagWritten = false;
foreach (MarkupObject subItem in property.Items)
{
if (!propertyTagWritten)
{
propertyTagWritten = true;
// uri is made addressable above so it is not necessary here
if (property.IsAttached || property.PropertyDescriptor == null)
{
Debug.Assert(!property.IsValueAsString, "Problem with MarkupObject implementation: String values cannnot be composite");
if (property.IsKey)
{
// When the reader supports <x:Key> ... </x:Key> format do the following:
// _writer.WriteStartElement("Key", NamespaceCache.XamlNamespace);
// The writer generates an exception for now:
throw new InvalidOperationException(SR.Format(SR.UnserializableKeyValue, property.Value.GetType().FullName));
}
else
{
string dpUri = scope.MakeAddressable(property.DependencyProperty.OwnerType);
WritePropertyStart(scope.GetPrefixOf(dpUri),
$"{property.DependencyProperty.OwnerType.Name}.{property.DependencyProperty.Name}", dpUri);
}
}
else
{
WritePropertyStart(prefix, $"{item.ObjectType.Name}.{property.PropertyDescriptor.Name}", uri);
writtenAttributes[property.Name] = property.Name;
}
explicitTagWritten = NeedToWriteExplicitTag(property, subItem);
if (explicitTagWritten)
{
WriteExplicitTagStart(property, scope);
}
}
WriteItem(subItem, new Scope(scope));
}
if (propertyTagWritten)
{
if (explicitTagWritten)
{
// Only write an end if we started the element
WriteExplicitTagEnd();
}
// Only write an end if we started the element
WritePropertyEnd();
}
}
}
if (contentProperty != null)
{
// If we have a content property, write it here
if (contentProperty.IsComposite)
{
IXmlSerializable serializable = contentProperty.Value as IXmlSerializable;
if (serializable != null)
{
WriteXmlIsland(serializable, scope);
}
else
{
bool lastWasString = false;
List<Type> wrapperTypes = GetWrapperTypes(contentProperty.PropertyType);
if (wrapperTypes == null)
{
foreach (MarkupObject subItem in contentProperty.Items)
{
if (!lastWasString && subItem.ObjectType == typeof(string) &&
!IsCollectionType(contentProperty.PropertyType) &&
!HasNonValueProperties(subItem))
{
_writer.WriteString(TextValue(subItem));
lastWasString = true;
}
else
{
WriteItem(subItem, new Scope(scope));
lastWasString = false;
}
}
}
else
{
foreach (MarkupObject subItem in contentProperty.Items)
{
MarkupProperty wrappedProperty = GetWrappedProperty(wrapperTypes, subItem);
if (wrappedProperty == null)
{
WriteItem(subItem, new Scope(scope));
lastWasString = false;
}
else if (wrappedProperty.IsComposite)
{
foreach (MarkupObject wrappedItem in wrappedProperty.Items)
{
if (!lastWasString && subItem.ObjectType == typeof(string) && !HasNonValueProperties(subItem))
{
_writer.WriteString(TextValue(wrappedItem));
lastWasString = true;
}
else
{
WriteItem(wrappedItem, new Scope(scope));
lastWasString = false;
}
}
}
else
{
if (!lastWasString)
{
_writer.WriteString(wrappedProperty.StringValue);
lastWasString = true;
}
else
{
WriteItem(subItem, new Scope(scope));
lastWasString = false;
}
}
}
}
}
}
else
{
string stringContent = contentProperty.Value as string;
if (stringContent == null)
{
stringContent = contentProperty.StringValue;
}
_writer.WriteString(stringContent);
}
writtenAttributes[contentProperty.Name] = contentProperty.Name;
}
// Write any defered content
if (deferredProperties != null)
{
// A property had a DependsOn attribute and we needed to defer it until
// we are sure the property was written.
foreach (MarkupProperty property in deferredProperties)
{
if (!writtenAttributes.ContainsKey(property.Name))
{
Debug.Assert(property.PropertyDescriptor != null);
writtenAttributes[property.Name] = property.Name;
_writer.WriteStartElement(prefix, $"{item.ObjectType.Name}.{property.PropertyDescriptor.Name}", uri);
if (property.IsComposite || property.StringValue.IndexOf('{') == 0)
{
foreach (MarkupObject subItem in property.Items)
{
WriteItem(subItem, new Scope(scope));
}
}
else
{
_writer.WriteString(property.StringValue);
}
_writer.WriteEndElement();
}
}
}
// Write the end of the element
_writer.WriteEndElement();
if( scope.IsTopOfSpacePreservationScope &&
_xmlTextWriter != null &&
_xmlTextWriter.Formatting != previousFormatting )
{
// Exiting a xml:space="preserve" scope. Restore formatting options
// if we are using a XmlTextWriter.
_xmlTextWriter.Formatting = previousFormatting;
}
}
private bool IsContentProperty(MarkupProperty property, ContentPropertyAttribute cpa, ref MarkupProperty contentProperty)
{
// if the property already knows it is the content property, we're done
bool isContentProperty = property.IsContent;
// the property may still be the content property by comparing it to the ContentPropertyAttribute
if (!isContentProperty)
{
PropertyDescriptor descriptor = property.PropertyDescriptor;
// FrameworkTemplate.VisualTree should be treated as Content
if (descriptor != null &&
typeof(FrameworkTemplate).IsAssignableFrom(descriptor.ComponentType) &&
property.Name == "Template" || property.Name == "VisualTree")
{
isContentProperty = true;
}
if (cpa != null &&
contentProperty == null &&
descriptor != null &&
descriptor.Name == cpa.Name)
{
if (property.IsComposite)
{
// We shouldn't serialize read-write IList properties as
// content because some IAddChild implementation do not
// recognize the collection itself as content, just the
// collection's elements.
if (descriptor == null ||
descriptor.IsReadOnly ||
!typeof(IList).IsAssignableFrom(descriptor.PropertyType))
{
isContentProperty = true;
}
}
else
{
// MarkupExtensions (including null) shouldn't be written
// out as content. In general, we don't want non-composite
// types to go out as content, because then they lose the
// property's type converter context. But for
// string/object type properties, we allow the value to go as
// content, so that it is possible to write out a
// non-normalized string. E.g. otherwise it would not be
// possible to write a Button or TextBox with tabs.
if (property.Value != null &&
!(property.Value is MarkupExtension) &&
property.PropertyType.IsAssignableFrom(typeof(string)) )
{
isContentProperty = true;
}
}
}
}
// if for either reason the property is the content property, then assign the ref argument
if (isContentProperty)
{
contentProperty = property;
}
return isContentProperty;
}
private bool IsDeferredProperty (MarkupProperty property, Dictionary<string, string> writtenAttributes,
ref PartiallyOrderedList<string, MarkupProperty> deferredProperties)
{
bool defer = false;
if (property.PropertyDescriptor != null)
{
// Since there can be multiple DependsOn attributes, we need to iterate
foreach (Attribute attribute in property.Attributes)
{
DependsOnAttribute dependsOn = attribute as DependsOnAttribute;
if (dependsOn != null)
{
if (!writtenAttributes.ContainsKey(dependsOn.Name))
{
// This property depends on a property that hasn't been written yet.
if (deferredProperties == null)
{
deferredProperties = new PartiallyOrderedList<string, MarkupProperty>();
}
// This uses a PartiallyOrderedList to ensure this property will appear
// after dependsOn.Name even if it also depends on property. If the
// DependsOn attributes are circular (e.g. A depends on B and B
// depends on A) the partially ordered list doesn't honor the oldest
// DependsOn attribute encountered resulting in a non-deterministic
// ordering. This error is not reported.
deferredProperties.SetOrder(dependsOn.Name, property.Name);
defer = true;
}
}
}
// If any of the properties this property depends on haven't been written yet
// we need to defer the attribute.
if (defer)
{
deferredProperties.Add(property.Name, property);
}
}
return defer;
}
/// <summary>
/// whether the property needs to write out an explicit tag because it is a collection value with a null default value
/// </summary>
private bool NeedToWriteExplicitTag(MarkupProperty property, MarkupObject firstItem)
{
// need to write an explicit tag if ALL of the following conditions are met:
// 1) property is a collection type
// 2) property has a DefaultValueAttribute
// 3) property's DefaultValueAttribute has a value of null
// 4) the first item under the property is not already an explicit tag
bool result = false;
// using this verbose syntax for returning, because we may need to add additional logic later
if (property.IsCollectionProperty)
{
if (_nullDefaultValueAttribute == null)
{
// if this hasn't been instantiated yet, instantiate it.
_nullDefaultValueAttribute = new DefaultValueAttribute(null);
}
if (property.Attributes.Contains(_nullDefaultValueAttribute))
{
result = true;
Object instance = firstItem.Instance;
if (instance is MarkupExtension)
{
if (instance is NullExtension)
{
// we allow null to be assigned as an explicit collection
result = false;
}
else if (property.PropertyType.IsArray)
{
// array extensions have to match up correctly, in case
// of an array of arrays
ArrayExtension arrayExt = instance as ArrayExtension;
if (property.PropertyType.IsAssignableFrom(arrayExt.Type.MakeArrayType()))
{
result = false;
}
}
}
else if (property.PropertyType.IsAssignableFrom(firstItem.ObjectType))
{
result = false;
}
}
}
return result;
}
/// <summary>
/// writes out the xml start tag for the given property's explicit collection
/// </summary>
private void WriteExplicitTagStart(MarkupProperty property, Scope scope)
{
Debug.Assert(property.Value != null);
Type tagType = property.Value.GetType();
string uri = scope.MakeAddressable(tagType);
string prefix = scope.GetPrefixOf(uri);
string name = tagType.Name;
if (typeof(MarkupExtension).IsAssignableFrom(tagType) &&
name.EndsWith("Extension", StringComparison.Ordinal))
{
// Prefer MarkupExtensions without the "Extension" postfix.
name = name.Substring(0, name.Length - EXTENSIONLENGTH);
}
_writer.WriteStartElement(prefix, name, uri);
}
/// <summary>
/// writes out the xml end tag for the given property's explicit collection
/// </summary>
private void WriteExplicitTagEnd()
{
_writer.WriteEndElement();
}
/// <summary>
/// writes out the xml start tag for a property
/// </summary>
private void WritePropertyStart(string prefix, string propertyName, string uri)
{
_writer.WriteStartElement(prefix, propertyName, uri);
}
/// <summary>
/// writes out the xml end tag for a property
/// </summary>
private void WritePropertyEnd()
{
_writer.WriteEndElement();
}
private void WriteXmlIsland(IXmlSerializable xmlSerializable, Scope scope)
{
scope.MakeAddressable(NamespaceCache.XamlNamespace);
_writer.WriteStartElement(scope.GetPrefixOf(NamespaceCache.XamlNamespace), XamlReaderHelper.DefinitionXDataTag, NamespaceCache.XamlNamespace);
xmlSerializable.WriteXml(_writer);
_writer.WriteEndElement();
}
private List<Type> GetWrapperTypes(Type type)
{
AttributeCollection attributes = TypeDescriptor.GetAttributes(type);
if (attributes[typeof(ContentWrapperAttribute)] == null)
return null;
else
{
List<Type> wrapperTypes = new List<Type>();
foreach (Attribute attribute in attributes)
{
ContentWrapperAttribute contentAttribute = attribute as ContentWrapperAttribute;
if (contentAttribute != null)
wrapperTypes.Add(contentAttribute.ContentWrapper);
}
return wrapperTypes;
}
}
private MarkupProperty GetWrappedProperty(List<Type> wrapperTypes, MarkupObject item)
{
if (!IsInTypes(item.ObjectType, wrapperTypes))
return null;
ContentPropertyAttribute cpa = item.Attributes[typeof(ContentPropertyAttribute)] as ContentPropertyAttribute;
MarkupProperty contentProperty = null;
foreach (MarkupProperty property in item.Properties)
{
if (property.IsContent || (cpa != null && property.PropertyDescriptor != null && property.PropertyDescriptor.Name == cpa.Name))
contentProperty = property;
else
{
// If we get any other property than the content property, need to serialize the wrapper type explicitly.
contentProperty = null;
break;
}
}
return contentProperty;
}
private bool IsInTypes(Type type, List<Type> types)
{
foreach (Type t in types)
if (t == type) return true;
return false;
}
private string TextValue(MarkupObject item)
{
foreach (MarkupProperty property in item.Properties)
{
if (property.IsValueAsString)
return property.StringValue;
break;
}
return null;
}
private bool HasNonValueProperties(MarkupObject item)
{
foreach (MarkupProperty property in item.Properties)
{
if (!property.IsValueAsString)
return true;
}
return false;
}
private bool IsCollectionType(Type type)
{
return typeof(IEnumerable).IsAssignableFrom(type) ||
typeof(Array).IsAssignableFrom(type);
}
private bool HasOnlyNormalizationNeutralStrings(MarkupProperty contentProperty,
bool keepLeadingSpace, bool keepTrailingSpace)
{
if (!contentProperty.IsComposite)
return IsNormalizationNeutralString(contentProperty.StringValue, keepLeadingSpace,
keepTrailingSpace);
else
{
// A leading space is only kept if it the string is just after an inline.
// A trailing space is only kept if it is just before an inline.
// To determine this we will examine the list until we find an item just after
// a string. We then have all the information necessary to determine if the
// string is normalization neutral. If any of the strings in the content are
// not normalization neutral then we we need a xml:space="preserve" so we terminate
// early.
bool result = true;
bool currentTrimSurroundingWhitespace = !keepLeadingSpace;
bool previousTrimSurroundingWhitespace = !keepLeadingSpace;
string text = null;
MarkupProperty nestedContentProperty = null;
List<Type> wrapperTypes = GetWrapperTypes(contentProperty.PropertyType);
foreach (MarkupObject subItem in contentProperty.Items)
{
previousTrimSurroundingWhitespace = currentTrimSurroundingWhitespace;
currentTrimSurroundingWhitespace = ShouldTrimSurroundingWhitespace(subItem);
if (text != null)
{
result = IsNormalizationNeutralString(text, !previousTrimSurroundingWhitespace,
!currentTrimSurroundingWhitespace);
text = null;
if (!result) return false;
}
if (nestedContentProperty != null)
{
result = HasOnlyNormalizationNeutralStrings(nestedContentProperty,
!previousTrimSurroundingWhitespace, !currentTrimSurroundingWhitespace);
nestedContentProperty = null;
if (!result) return false;
}
if (subItem.ObjectType == typeof(string))
{
text = TextValue(subItem);
if (text != null)
continue;
}
if (wrapperTypes != null)
{
MarkupProperty wrappedProperty = GetWrappedProperty(wrapperTypes, subItem);
if (wrappedProperty != null)
{
nestedContentProperty = wrappedProperty;
continue;
}
}
}
if (text != null)
{
// The last element was a string. Determine if it is normalization neutural.
result = IsNormalizationNeutralString(text, !previousTrimSurroundingWhitespace,
keepTrailingSpace);
}
else if (nestedContentProperty != null)
{
// The last element was a wrapped element. Determine if it is normalization neutural.
result = HasOnlyNormalizationNeutralStrings(nestedContentProperty,
!previousTrimSurroundingWhitespace, keepTrailingSpace);
}
return result;
}
}
private bool ShouldTrimSurroundingWhitespace(MarkupObject item)
{
// An item declares how surrounding whitespace should be treated with the TrimSurroundingWhitespaceAttribute.
TrimSurroundingWhitespaceAttribute attribute = item.Attributes[typeof(TrimSurroundingWhitespaceAttribute)] as TrimSurroundingWhitespaceAttribute;
return (attribute != null);
}
private bool IsNormalizationNeutralString(string value, bool keepLeadingSpace, bool keepTrailingSpace)
{
bool lastCharSpace = !keepLeadingSpace;
for (int textIndex = 0; textIndex < value.Length; textIndex++)
{
char currentChar = value[textIndex];
switch (currentChar)
{
case ' ':
if (lastCharSpace)
// two spaces or more spaces in a row (or a leading space) will be normalized
return false;
lastCharSpace = true;
break;
case '\t':
case '\r':
case '\n':
case '\f':
// These will be turned into spaces.
return false;
default:
// Anything else is left alone.
lastCharSpace = false;
break;
// Chinese/Japanese and Thai rules relate to LF handling and any LF character
// will be treated as non-neutral so they don't need special handling.
}
}
return !lastCharSpace || keepTrailingSpace;
}
/// <summary>
/// Private class used by Scope to record a prefix to URI mapping.
/// </summary>
private class Mapping
{
public readonly string Uri;
public readonly string Prefix;
public Mapping(string uri, string prefix)
{
Uri = uri;
Prefix = prefix;
}
public override bool Equals(object obj)
{
Mapping other = obj as Mapping;
return other != null && Uri.Equals(other.Uri) && Prefix.Equals(other.Prefix);
}
public override int GetHashCode()
{
return Uri.GetHashCode() + Prefix.GetHashCode();
}
}
/// <summary>
/// Private class used by MarkupWriter to record xmlns defintions emitted
/// by the writer.
/// </summary>
private class Scope
{
private Scope _containingScope;
private bool? _xmlnsSpacePreserve;
private Dictionary<string, string> _uriToPrefix;
private Dictionary<string, string> _prefixToUri;
public Scope(Scope containingScope)
{
_containingScope = containingScope;
}
public bool XmlnsSpacePreserve
{
get
{
if (_xmlnsSpacePreserve != null)
return (bool)_xmlnsSpacePreserve;
else if (_containingScope != null)
return _containingScope.XmlnsSpacePreserve;
else
return false;
}
set
{
_xmlnsSpacePreserve = value;
}
}
// Returns true if this scope is the top of a space preservation scope.
// True if:
// 1) This is the outermost scope. (No containing scope)
// 2) The containing scope has a different space preservation setting
// than the current scope.
// This information is derived from _xmlnsSpacePreserve and _containingScope
// to avoid memory cost of tracking additional object state.
public bool IsTopOfSpacePreservationScope
{
get
{
if( _containingScope == null )
{
// Topmost element is top of a scope by definition.
return true;
}
else if( _xmlnsSpacePreserve == null )
{
// Most common case - this scope inherits the parent's
// scope, so it's not a top level preservation scope.
return false;
}
else if( ((bool)_xmlnsSpacePreserve) != _containingScope.XmlnsSpacePreserve )
{
// Our specified preservation setting does not match containing scope's
// preservation setting - we're top of our specific preservations cope.
return true;
}
// We have a specified preservation setting that matches the
// containing scope's preservation setting.
Debug.Assert( ((bool)_xmlnsSpacePreserve) == _containingScope.XmlnsSpacePreserve ,
"At this point the space preservation settings should be equal - did somebody break the logic above us?");
return false;
}
}
public string GetPrefixOf(string uri)
{
string result;
if (_uriToPrefix != null && _uriToPrefix.TryGetValue(uri, out result))
return result;
if (_containingScope != null)
return _containingScope.GetPrefixOf(uri);
return null;
}
public string GetUriOf(string prefix)
{
string result;
if (_prefixToUri != null && _prefixToUri.TryGetValue(prefix, out result))
return result;
if (_containingScope != null)
return _containingScope.GetUriOf(prefix);
return null;
}
public void RecordMapping(string prefix, string uri)
{
if (_uriToPrefix == null)
_uriToPrefix = new Dictionary<string, string>();
if (_prefixToUri == null)
_prefixToUri = new Dictionary<string, string>();
_uriToPrefix[uri] = prefix;
_prefixToUri[prefix] = uri;
}
public void MakeAddressable(IEnumerable<Type> types)
{
if (types != null)
foreach (Type type in types)
MakeAddressable(type);
}
public string MakeAddressable(Type type)
{
return MakeAddressable(NamespaceCache.GetNamespaceUriFor(type));
}
public string MakeAddressable(string uri)
{
if (GetPrefixOf(uri) == null)
{
string basePrefix = NamespaceCache.GetDefaultPrefixFor(uri);
string prefix = basePrefix;
int i = 0;
while (GetUriOf(prefix) != null)
prefix = string.Concat(basePrefix, i++);
RecordMapping(prefix, uri);
}
return uri;
}
public IEnumerable<Mapping> EnumerateLocalMappings
{
get
{
if (_uriToPrefix != null)
foreach (KeyValuePair<string, string> mapping in _uriToPrefix)
yield return new Mapping(mapping.Key, mapping.Value);
}
}
public IEnumerable<Mapping> EnumerateAllMappings
{
get
{
if (_containingScope != null)
foreach (Mapping mapping in _containingScope.EnumerateAllMappings)
yield return mapping;
foreach (Mapping mapping in EnumerateLocalMappings)
yield return mapping;
}
}
}
/// <summary>
/// A IValueSerializerContext that provides a serializer for Type's.
/// </summary>
private class MarkupWriterContext : IValueSerializerContext
{
Scope _scope;
internal MarkupWriterContext(Scope scope)
{
_scope = scope;
}
public ValueSerializer GetValueSerializerFor(PropertyDescriptor descriptor)
{
if (descriptor.PropertyType == typeof(Type))
return new TypeValueSerializer(_scope);
else
return ValueSerializer.GetSerializerFor(descriptor);
}
public ValueSerializer GetValueSerializerFor(Type type)
{
if (type == typeof(Type))
return new TypeValueSerializer(_scope);
else
return ValueSerializer.GetSerializerFor(type);
}
public IContainer Container
{
get { return null; }
}
public object Instance
{
get { return null; }
}
public void OnComponentChanged()
{
}
public bool OnComponentChanging()
{
return true;
}
public PropertyDescriptor PropertyDescriptor
{
get { return null; }
}
public object GetService(Type serviceType)
{
return null;
}
}
/// <summary>
/// A TypeValueSerializer that converts a Type instance to a string using
/// the Scope.
/// </summary>
private class TypeValueSerializer : ValueSerializer
{
Scope _scope;
public TypeValueSerializer(Scope scope)
{
_scope = scope;
}
public override bool CanConvertToString(object value, IValueSerializerContext context)
{
return true;
}
public override string ConvertToString(object value, IValueSerializerContext context)
{
Type type = value as Type;
if (type == null)
throw new InvalidOperationException();
string uri = _scope.MakeAddressable(type);
string prefix = _scope.GetPrefixOf(uri);
if (prefix == null || prefix == "")
return type.Name;
else
return $"{prefix}:{type.Name}";
}
public override IEnumerable<Type> TypeReferences(object value, IValueSerializerContext context)
{
Type type = value as Type;
if (type != null)
return new Type[] { type };
else
return base.TypeReferences(value, context);
}
}
/// <summary>
/// A private cache of namespace information derived from assemblies and their XmlnsDefintionAttribute's.
/// </summary>
private static class NamespaceCache
{
private static Dictionary<Assembly, Dictionary<string, string>> XmlnsDefinitions = new Dictionary<Assembly, Dictionary<string, string>>();
private static Dictionary<string, string> DefaultPrefixes = new Dictionary<string, string>();
private static readonly object SyncObject = new object();
static Dictionary<string, string> GetMappingsFor(Assembly assembly)
{
Dictionary<string, string> namespaceToUri;
lock (SyncObject)
{
if (!XmlnsDefinitions.TryGetValue(assembly, out namespaceToUri))
{
foreach (XmlnsPrefixAttribute prefix in assembly.GetCustomAttributes(typeof(XmlnsPrefixAttribute), true))
DefaultPrefixes[prefix.XmlNamespace] = prefix.Prefix;
namespaceToUri = new Dictionary<string, string>();
XmlnsDefinitions[assembly] = namespaceToUri;
Object[] customAttrs = assembly.GetCustomAttributes(typeof(XmlnsDefinitionAttribute), true);
foreach (XmlnsDefinitionAttribute definition in customAttrs)
{
if (definition.AssemblyName == null)
{
string previousBestNamespace = null;
string previousBestPrefix = null;
string newPrefix = null;
// if multiple namespaces for the type exits then give shorter prefix
// definitions precedence.
if(namespaceToUri.TryGetValue(definition.ClrNamespace, out previousBestNamespace))
{
if(DefaultPrefixes.TryGetValue(previousBestNamespace, out previousBestPrefix))
{
DefaultPrefixes.TryGetValue(definition.XmlNamespace, out newPrefix);
}
}
if ( (null == previousBestNamespace) || (null == previousBestPrefix) ||
(null != newPrefix && previousBestPrefix.Length > newPrefix.Length) )
{
namespaceToUri[definition.ClrNamespace] = definition.XmlNamespace;
}
}
else
{
Assembly referencedAssembly = Assembly.Load(new AssemblyName(definition.AssemblyName));
if (referencedAssembly != null)
{
Dictionary<string, string> assembliesNamespacetoUri = GetMappingsFor(referencedAssembly);
assembliesNamespacetoUri[definition.ClrNamespace] = definition.XmlNamespace;
}
}
}
}
}
return namespaceToUri;
}
public static string GetNamespaceUriFor(Type type)
{
string result;
lock (SyncObject)
{
if (type.Namespace == null)
{
result = $"{clrUriPrefix};assembly={type.Assembly.GetName().Name}";
}
else
{
Dictionary<string, string> namespaceToUri = GetMappingsFor(type.Assembly);
if (!namespaceToUri.TryGetValue(type.Namespace, out result))
{
result = $"{clrUriPrefix}{type.Namespace};assembly={type.Assembly.GetName().Name}";
}
}
}
return result;
}
public static string GetDefaultPrefixFor(string uri)
{
string result;
lock (SyncObject)
{
DefaultPrefixes.TryGetValue(uri, out result);
if (result == null)
{
result = "assembly";
if (uri.StartsWith(clrUriPrefix, StringComparison.Ordinal))
{
ReadOnlySpan<char> ns = uri.AsSpan(clrUriPrefix.Length, uri.IndexOf(';') - clrUriPrefix.Length);
StringBuilder r = new StringBuilder();
for (int i = 0; i < ns.Length; i++)
{
char c = ns[i];
if (c >= 'A' && c <= 'Z')
r.Append(char.ToLowerInvariant(c));
}
if (r.Length > 0)
result = r.ToString();
}
}
}
return result;
}
public static string XamlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml";
public static string XmlNamespace = "http://www.w3.org/XML/1998/namespace";
public static string XmlnsNamespace = "http://www.w3.org/2000/xmlns/";
}
#endregion
private XmlWriter _writer;
// Same reference as _writer, but cast to a XmlTextWriter. This is to work
// around a XmlTextWriter-specific issue where it would inject extra whitespaces
// to make XML human-readable even when whitespaces are significant.
// (xml:space="preserve" is set to true.)
private XmlTextWriter _xmlTextWriter;
private static DefaultValueAttribute _nullDefaultValueAttribute;
}
}
|