using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Xml.Serialization; namespace OpenTap { /// /// This attribute is used to dynamically embed the properties of an object into another object. /// /// /// A property of type PT declared on a type DT decorated with this attribute will not be visible in reflection information (ITypeData) for DT. /// Instead all properties declared on PT will be visible on DT as though they had been declared there. /// [AttributeUsage(AttributeTargets.Property)] public class EmbedPropertiesAttribute : Attribute { /// /// When true, property name of the owning property is used as prefix for embedded properties. E.g., the name will be 'EmbeddedProperty.X'. /// A prefix can help prevent name-clashing issues if multiple properties gets the same name. /// public bool PrefixPropertyName { get; set; } = true; /// /// Custom prefix for embedded properties. This will overwrite PrefixPropertyName. /// A prefix can help prevent name-clashing issues if multiple properties gets the same name. /// public string Prefix { get; set; } = null; } // // The code below implements the embedded member data dynamic reflection. // It creates a wrapper around existing types and gives them an extra layer // which contains the added properties. // class EmbeddedMemberData : IMemberData, IDynamicMemberData, IEmbeddedMemberData { public IMemberData OwnerMember => ownerMember; public IMemberData InnerMember => innerMember; readonly IMemberData ownerMember; readonly IMemberData innerMember; private readonly IEnumerable additionalAttributes; public ITypeData DeclaringType => ownerMember.DeclaringType; public ITypeData TypeDescriptor => innerMember.TypeDescriptor; public bool Writable => innerMember.Writable; public bool Readable => innerMember.Readable; public IEnumerable Attributes => attributes ?? (attributes = loadAndTransformAttributes()); public string Name { get; } public object GetValue(object owner) { var target = ownerMember.GetValue(owner); if (target == null) return null; return innerMember.GetValue(target); } public void SetValue(object owner, object value) { var target = ownerMember.GetValue(owner); if (target == null) throw new NullReferenceException($"Embedded property object owner ('{ownerMember.Name}') is null."); innerMember.SetValue(target, value); } public EmbeddedMemberData(IMemberData ownerMember, IMemberData innerMember, IEnumerable additionalAttributes) { this.ownerMember = ownerMember; this.innerMember = innerMember; this.additionalAttributes = additionalAttributes; var embed = ownerMember.GetAttribute(); if (embed.PrefixPropertyName) { var prefix_name = embed.Prefix ?? ownerMember.Name; Name = prefix_name + "." + this.innerMember.Name; } else Name = this.innerMember.Name; } public override string ToString() => $"EmbMem:{Name}"; object[] attributes; /// /// Loads and transform the list of attributes. /// Some attributes are sensitive to naming, the known ones are AvailableValuesAttribute, /// SuggestedValueAttribute, and EnabledIf. Others might exist, but they are, for now, not supported. /// When NoPrefix is set on EmbedProperties, then there is no issue, but with the prefix, those names also needs /// to be transformed. /// Additionally, there is some special behavior wanted for Display, for usability and to avoid name clashing: /// - If a Display name/group is set on the owner property, that should turn into a group for the embedded properties. /// - If there is no display name, use the prefix name for the group. /// - If there is no prefix name, don't touch DisplayAttribute. /// object[] loadAndTransformAttributes() { string prefix_name = null; var list = innerMember.Attributes.ToList(); var embed = ownerMember.GetAttribute(); if (embed.PrefixPropertyName) prefix_name = embed.Prefix ?? ownerMember.Name; string[] pre_group1; string pre_name; double ownerOrder = -10000; bool collapsed = false; { var owner_display = ownerMember.GetAttribute(); if (owner_display == null) { owner_display = new DisplayAttribute(embed.PrefixPropertyName ? ownerMember.Name : ""); pre_group1 = owner_display.Group; pre_name = owner_display.Name; } else { collapsed = owner_display.Collapsed; ownerOrder = owner_display.Order; pre_group1 = owner_display.Group; pre_name = owner_display.Name; } } string name; string[] post_group; var d = list.OfType().FirstOrDefault(); if (d != null) { list.Remove(d); // need to re-add a new DisplayAttribute. name = d.Name; post_group = d.Group; if (d.Collapsed) collapsed = true; } else { name = Name; post_group = Array.Empty(); } string[] prefixGroups; if (embed.PrefixPropertyName == false && string.IsNullOrWhiteSpace(pre_name)) prefixGroups = Array.Empty(); else prefixGroups = new[] {pre_name}; var groups = pre_group1.Concat(prefixGroups).Concat(post_group).ToArray(); double order = -10000; { // calculate order: // if owner order is set, add it to the display order. if (d != null && d.Order != -10000.0) { order = d.Order; if (ownerOrder != -10000.0) { // if an order is specified, add the two orders together. // this makes sure that property groups can be arranged. order += ownerOrder; } } else { order = ownerOrder; } } d = new DisplayAttribute(name, Groups: groups, Description: d?.Description, Order: order, Collapsed: collapsed); list.Add(d); if (prefix_name != null) { // Transform properties that has issues with embedding. for (var i = 0; i < list.Count; i++) { var item = list[i]; if (item is AvailableValuesAttribute avail) { list[i] = new AvailableValuesAttribute(prefix_name + "." + avail.PropertyName); } else if (item is SuggestedValuesAttribute sug) { list[i] = new SuggestedValuesAttribute(prefix_name + "." + sug.PropertyName); } else if (item is EnabledIfAttribute enabled) { list[i] = new EnabledIfAttribute(prefix_name + "." + enabled.PropertyName, enabled.Values) {HideIfDisabled = enabled.HideIfDisabled}; } } } return attributes = list.Concat(additionalAttributes ?? []).ToArray(); } bool IDynamicMemberData.IsDisposed => OwnerMember is IDynamicMemberData dyn && dyn.IsDisposed; } class EmbeddedTypeData : ITypeData { IMemberData[] listedEmbeddedMembers; public ITypeData BaseType { get; set; } public bool CanCreateInstance => BaseType.CanCreateInstance; public IEnumerable Attributes => BaseType.Attributes; public string Name => EmbeddedTypeDataProvider.exp + BaseType.Name; public object CreateInstance(object[] arguments) => BaseType.CreateInstance(arguments); public IMemberData GetMember(string name) => GetMembers().FirstOrDefault(x => x.Name == name); public IEnumerable GetMembers() => BaseType.GetMembers() .Where(x => x.HasAttribute() == false) .Concat(listedEmbeddedMembers ??= ListEmbeddedMembers()); public override int GetHashCode() => BaseType.GetHashCode() * 86966513 + typeof(EmbeddedTypeData).GetHashCode(); public override bool Equals(object obj) { if(obj is EmbeddedTypeData e) return e.BaseType == BaseType; return false; } [ThreadStatic] static HashSet currentlyListing; public IEnumerable GetEmbeddingMembers() { foreach (var member in BaseType.GetMembers()) { if (member.HasAttribute()) { yield return member; } } } internal IMemberData[] ListEmbeddedMembers() { if (currentlyListing == null) currentlyListing = new HashSet(); List embeddedMembers = new List(); if (currentlyListing.Contains(this)) return embeddedMembers.ToArray(); currentlyListing.Add(this); foreach (var member in BaseType.GetMembers()) { if (member.HasAttribute()) { var xmlIgnore = member.HasAttribute(); var members = member.TypeDescriptor.GetMembers(); foreach(var m in members) { if (m.HasAttribute()) { if (xmlIgnore == false) xmlIgnore = m.HasAttribute(); members = EmbeddedTypeDataProvider.FromTypeData(member.TypeDescriptor).GetMembers(); break; } } object[] additionalAttributes = xmlIgnore ? [new XmlIgnoreAttribute()] : []; foreach (var innermember in members) embeddedMembers.Add(new EmbeddedMemberData(member, innermember, additionalAttributes)); } } currentlyListing.Remove(this); return embeddedMembers.ToArray(); } public override string ToString() => $"EmbType:{Name}"; } class EmbeddedTypeDataProvider : IStackedTypeDataProvider { public double Priority => 10.5; internal const string exp = "emb:"; static ConditionalWeakTable internedValues = new ConditionalWeakTable(); static EmbeddedTypeData getOrCreate(ITypeData e) => internedValues.GetValue(e, t => t.GetMembers().Any(m => m.HasAttribute()) ? new EmbeddedTypeData{BaseType = e} : null); public static EmbeddedTypeData FromTypeData(ITypeData baseType) => getOrCreate(baseType); public ITypeData GetTypeData(string identifier, TypeDataProviderStack stack) { if (identifier.StartsWith(exp)) { var tp = TypeData.GetTypeData(identifier.Substring(exp.Length)); if (tp != null) { var embTp = FromTypeData(tp); if (embTp != null) return embTp; return tp; } } return null; } class KeyBox { public int Key { get; set; } } // this key table is used to keep track of the members of dynamic types. // if the key has changed, then it means that the cache should be invalidated. static ConditionalWeakTable keyTable = new ConditionalWeakTable(); public ITypeData GetTypeData(object obj, TypeDataProviderStack stack) { var typeData = stack.GetTypeData(obj); var nowKey = DynamicMember.GetTypeDataKey(obj); bool reload = false; if (nowKey != 0) { if (keyTable.TryGetValue(obj, out var currentKey)) { if (currentKey.Key != nowKey) { reload = true; currentKey.Key = nowKey; } } else { keyTable.Add(obj, new KeyBox(){Key = nowKey}); reload = true; } } if (reload) { internedValues.Remove(typeData); } if (internedValues.TryGetValue(typeData, out EmbeddedTypeData val)) { return val ?? typeData; } if (typeData.GetMembers().Any(x => x.HasAttribute())) return FromTypeData(typeData); // assign null to the value. return internedValues.GetValue(typeData, t => null) ?? typeData; } } }