From 91589c9e6e34c709782ad829011629b5dfad1f76 Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 20:29:14 +0200 Subject: [PATCH 1/9] retrieve enum case name only once --- src/Humanizer/EnumHumanizeExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Humanizer/EnumHumanizeExtensions.cs b/src/Humanizer/EnumHumanizeExtensions.cs index b4891c463..5a7e6e90f 100644 --- a/src/Humanizer/EnumHumanizeExtensions.cs +++ b/src/Humanizer/EnumHumanizeExtensions.cs @@ -19,7 +19,8 @@ public static class EnumHumanizeExtensions public static string Humanize(this Enum input) { Type type = input.GetType(); - var memInfo = type.GetMember(input.ToString()); + var caseName = input.ToString(); + var memInfo = type.GetMember(caseName); if (memInfo.Length > 0) { @@ -29,7 +30,7 @@ public static string Humanize(this Enum input) return customDescription; } - return input.ToString().Humanize(); + return caseName.Humanize(); } // I had to add this method because PCL doesn't have DescriptionAttribute & I didn't want two versions of the code & thus the reflection From a5bae33edc9d4e86f7ecd42d95ab9cfcaeab7ec3 Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 22:06:13 +0200 Subject: [PATCH 2/9] allow to override enum description attribute property name per type --- ...provalTest.approve_public_api.approved.txt | 1 + ...WithCustomDescriptionPropertyNamesTests.cs | 31 +++++++++++++++++++ src/Humanizer.Tests/EnumUnderTest.cs | 13 ++++++++ src/Humanizer.Tests/Humanizer.Tests.csproj | 1 + src/Humanizer/Configuration/Configurator.cs | 16 ++++++++++ src/Humanizer/EnumHumanizeExtensions.cs | 14 ++++++--- 6 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt index 91a70936d..08b5fab23 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt @@ -75,6 +75,7 @@ public class Configurator { public Humanizer.Configuration.LocaliserRegistry CollectionFormatters { get; } public Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } + public System.Collections.Generic.IDictionary EnumDescriptionPropertyNames { get; } public Humanizer.Configuration.LocaliserRegistry Formatters { get; } public Humanizer.Configuration.LocaliserRegistry NumberToWordsConverters { get; } public Humanizer.Configuration.LocaliserRegistry Ordinalizers { get; } diff --git a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs new file mode 100644 index 000000000..fa57db09c --- /dev/null +++ b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs @@ -0,0 +1,31 @@ +using System; +using Humanizer.Configuration; +using Xunit; + +namespace Humanizer.Tests +{ + public class EnumHumanizeWithCustomDescriptionPropertyNamesTests : IDisposable + { + public EnumHumanizeWithCustomDescriptionPropertyNamesTests() + { + Configurator.EnumDescriptionPropertyNames[typeof (EnumUnderTest)] = "Info"; + } + + public void Dispose() + { + Configurator.EnumDescriptionPropertyNames.Remove(typeof (EnumUnderTest)); + } + + [Fact] + public void HonorsCustomPropertyAttribute() + { + Assert.Equal(EnumTestsResources.MemberWithCustomPropertyAttribute, EnumUnderTest.MemberWithCustomPropertyAttribute.Humanize()); + } + + [Fact] + public void CanHumanizeMembersWithoutDescriptionAttribute() + { + Assert.Equal(EnumTestsResources.MemberWithoutDescriptionAttributeSentence, EnumUnderTest.MemberWithoutDescriptionAttribute.Humanize()); + } + } +} \ No newline at end of file diff --git a/src/Humanizer.Tests/EnumUnderTest.cs b/src/Humanizer.Tests/EnumUnderTest.cs index 1d7cd06fd..8f9423804 100644 --- a/src/Humanizer.Tests/EnumUnderTest.cs +++ b/src/Humanizer.Tests/EnumUnderTest.cs @@ -13,6 +13,8 @@ public enum EnumUnderTest MemberWithCustomDescriptionAttribute, [ImposterDescription(42)] MemberWithImposterDescriptionAttribute, + [CustomProperty(EnumTestsResources.MemberWithCustomPropertyAttribute)] + MemberWithCustomPropertyAttribute, MemberWithoutDescriptionAttribute, ALLCAPITALS } @@ -23,6 +25,7 @@ public class EnumTestsResources public const string MemberWithDescriptionAttributeSubclass = "Description in Description subclass"; public const string MemberWithCustomDescriptionAttribute = "Description in custom Description attribute"; public const string MemberWithImposterDescriptionAttribute = "Member with imposter description attribute"; + public const string MemberWithCustomPropertyAttribute = "Description in custom property attribute"; public const string MemberWithoutDescriptionAttributeSentence = "Member without description attribute"; public const string MemberWithoutDescriptionAttributeTitle = "Member Without Description Attribute"; public const string MemberWithoutDescriptionAttributeLowerCase = "member without description attribute"; @@ -59,4 +62,14 @@ public override string Description get { return "Overridden " + base.Description; } } } + + public class CustomPropertyAttribute : Attribute + { + public string Info { get; set; } + + public CustomPropertyAttribute(string info) + { + Info = info; + } + } } \ No newline at end of file diff --git a/src/Humanizer.Tests/Humanizer.Tests.csproj b/src/Humanizer.Tests/Humanizer.Tests.csproj index cd7832059..c206f8c2b 100644 --- a/src/Humanizer.Tests/Humanizer.Tests.csproj +++ b/src/Humanizer.Tests/Humanizer.Tests.csproj @@ -63,6 +63,7 @@ + diff --git a/src/Humanizer/Configuration/Configurator.cs b/src/Humanizer/Configuration/Configurator.cs index 227e37b4f..d7c28f835 100644 --- a/src/Humanizer/Configuration/Configurator.cs +++ b/src/Humanizer/Configuration/Configurator.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Humanizer.DateTimeHumanizeStrategy; using Humanizer.Localisation.Formatters; using Humanizer.Localisation.NumberToWords; @@ -98,5 +100,19 @@ public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy get { return _dateTimeHumanizeStrategy; } set { _dateTimeHumanizeStrategy = value; } } + + private static readonly Dictionary _enumDescriptionPropertyNames = new Dictionary(); + internal static string EnumDescriptionPropertyNameFor(Type type) + { + string result; + return _enumDescriptionPropertyNames.TryGetValue(type, out result) ? result : null; + } + /// + /// The registry of custom attribute property names for Enum.Humanize + /// + public static IDictionary EnumDescriptionPropertyNames + { + get { return _enumDescriptionPropertyNames; } + } } } diff --git a/src/Humanizer/EnumHumanizeExtensions.cs b/src/Humanizer/EnumHumanizeExtensions.cs index 5a7e6e90f..12f5fc14a 100644 --- a/src/Humanizer/EnumHumanizeExtensions.cs +++ b/src/Humanizer/EnumHumanizeExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reflection; +using Humanizer.Configuration; namespace Humanizer { @@ -9,7 +10,8 @@ namespace Humanizer /// public static class EnumHumanizeExtensions { - private static readonly Func DescriptionProperty = p => p.Name == "Description" && p.PropertyType == typeof (string); + private const string DescriptionPropertyName = "Description"; + private static readonly Func StringTypedProperty = p => p.PropertyType == typeof(string); /// /// Turns an enum member into a human readable string; e.g. AnonymousUser -> Anonymous user. It also honors DescriptionAttribute data annotation @@ -24,7 +26,8 @@ public static string Humanize(this Enum input) if (memInfo.Length > 0) { - var customDescription = GetCustomDescription(memInfo[0]); + var propertyName = Configurator.EnumDescriptionPropertyNameFor(type) ?? DescriptionPropertyName; + var customDescription = GetCustomDescription(memInfo[0], propertyName); if (customDescription != null) return customDescription; @@ -34,14 +37,17 @@ public static string Humanize(this Enum input) } // I had to add this method because PCL doesn't have DescriptionAttribute & I didn't want two versions of the code & thus the reflection - private static string GetCustomDescription(MemberInfo memberInfo) + private static string GetCustomDescription(MemberInfo memberInfo, string propertyName) { var attrs = memberInfo.GetCustomAttributes(true); foreach (var attr in attrs) { var attrType = attr.GetType(); - var descriptionProperty = attrType.GetProperties().FirstOrDefault(DescriptionProperty); + var descriptionProperty = + attrType.GetProperties() + .Where(StringTypedProperty) + .FirstOrDefault(p => p.Name == propertyName); if (descriptionProperty != null) return descriptionProperty.GetValue(attr, null).ToString(); } From cd9c8cd696c9a15b5144e6a8e7ae687ef9abfff4 Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 22:21:17 +0200 Subject: [PATCH 3/9] move default property name into configurator --- src/Humanizer/Configuration/Configurator.cs | 5 +++-- src/Humanizer/EnumHumanizeExtensions.cs | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Humanizer/Configuration/Configurator.cs b/src/Humanizer/Configuration/Configurator.cs index d7c28f835..4c2b41c8a 100644 --- a/src/Humanizer/Configuration/Configurator.cs +++ b/src/Humanizer/Configuration/Configurator.cs @@ -101,11 +101,12 @@ public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy set { _dateTimeHumanizeStrategy = value; } } + private const string DefaultEnumDescriptionPropertyName = "Description"; private static readonly Dictionary _enumDescriptionPropertyNames = new Dictionary(); internal static string EnumDescriptionPropertyNameFor(Type type) { - string result; - return _enumDescriptionPropertyNames.TryGetValue(type, out result) ? result : null; + string result = _enumDescriptionPropertyNames.TryGetValue(type, out result) ? result : null; + return result ?? DefaultEnumDescriptionPropertyName; } /// /// The registry of custom attribute property names for Enum.Humanize diff --git a/src/Humanizer/EnumHumanizeExtensions.cs b/src/Humanizer/EnumHumanizeExtensions.cs index 12f5fc14a..a143a377f 100644 --- a/src/Humanizer/EnumHumanizeExtensions.cs +++ b/src/Humanizer/EnumHumanizeExtensions.cs @@ -10,7 +10,6 @@ namespace Humanizer /// public static class EnumHumanizeExtensions { - private const string DescriptionPropertyName = "Description"; private static readonly Func StringTypedProperty = p => p.PropertyType == typeof(string); /// @@ -26,7 +25,7 @@ public static string Humanize(this Enum input) if (memInfo.Length > 0) { - var propertyName = Configurator.EnumDescriptionPropertyNameFor(type) ?? DescriptionPropertyName; + var propertyName = Configurator.EnumDescriptionPropertyNameFor(type); var customDescription = GetCustomDescription(memInfo[0], propertyName); if (customDescription != null) From 67018e6eee6afd489c46b5c74d6cc6fb6a715b50 Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 22:24:54 +0200 Subject: [PATCH 4/9] add PR to release notes --- release_notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release_notes.md b/release_notes.md index 3d3900d0a..4d04ca70d 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,4 +1,5 @@ ###In Development + - [#257](https://github.com/MehdiK/Humanizer/pull/277): Added support for custom enum description attribute property names [Commits](https://github.com/MehdiK/Humanizer/compare/v1.26.1...master) From 107f1b4f4f279a310637593f9ae77ed7b0648197 Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 22:41:08 +0200 Subject: [PATCH 5/9] update readme --- readme.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index afc6832a8..af6aca854 100644 --- a/readme.md +++ b/readme.md @@ -163,10 +163,10 @@ Calling `ToString` directly on enum members usually results in less than ideal o ```C# public enum EnumUnderTest { - [Description("Custom description")] - MemberWithDescriptionAttribute, - MemberWithoutDescriptionAttribute, - ALLCAPITALS +[Description("Custom description")] +MemberWithDescriptionAttribute, +MemberWithoutDescriptionAttribute, +ALLCAPITALS } ``` @@ -186,6 +186,10 @@ EnumUnderTest.MemberWithoutDescriptionAttribute.Humanize().Transform(To.TitleCas You are not limited to `DescriptionAttribute` for custom description. Any attribute applied on enum members with a `string Description` property counts. This is to help with platforms with missing `DescriptionAttribute` and also for allowing subclasses of the `DescriptionAttribute`. +You can even configure the name of the property of attibute to use as description. + +`Configurator.EnumDescriptionPropertyNames[typeof(EnumUnderTest)] = "Info"` + Hopefully this will help avoid littering enums with unnecessary attributes! ###Dehumanize Enums From 83ea7baf208b5c211558d3457372b51b5ae2d7fb Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 23:38:58 +0200 Subject: [PATCH 6/9] suppress code analysis message for IDisposable in test --- .../EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs index fa57db09c..ea7e201c8 100644 --- a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs +++ b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs @@ -11,6 +11,8 @@ public EnumHumanizeWithCustomDescriptionPropertyNamesTests() Configurator.EnumDescriptionPropertyNames[typeof (EnumUnderTest)] = "Info"; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", + Justification = "This is a test only class, and doesn't need a 'proper' IDisposable implementation.")] public void Dispose() { Configurator.EnumDescriptionPropertyNames.Remove(typeof (EnumUnderTest)); From 67aa8b093a9d5e8890f7325efd39fb1a348d8d4f Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 23:45:10 +0200 Subject: [PATCH 7/9] restore indentation in enum example --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index af6aca854..e6aead3f4 100644 --- a/readme.md +++ b/readme.md @@ -163,10 +163,10 @@ Calling `ToString` directly on enum members usually results in less than ideal o ```C# public enum EnumUnderTest { -[Description("Custom description")] -MemberWithDescriptionAttribute, -MemberWithoutDescriptionAttribute, -ALLCAPITALS + [Description("Custom description")] + MemberWithDescriptionAttribute, + MemberWithoutDescriptionAttribute, + ALLCAPITALS } ``` From 608c041e6ccc11658b223f60ddc2ed010f56a338 Mon Sep 17 00:00:00 2001 From: Max Malook Date: Fri, 23 May 2014 23:51:37 +0200 Subject: [PATCH 8/9] suppress code analysis message for IDisposable in test --- .../EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs index ea7e201c8..86c664564 100644 --- a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs +++ b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs @@ -4,6 +4,8 @@ namespace Humanizer.Tests { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", + Justification = "This is a test only class, and doesn't need a 'proper' IDisposable implementation.")] public class EnumHumanizeWithCustomDescriptionPropertyNamesTests : IDisposable { public EnumHumanizeWithCustomDescriptionPropertyNamesTests() From 93a80ea6d5c80e01f3f5bcd6415a0d6bd0ffd77e Mon Sep 17 00:00:00 2001 From: Max Malook Date: Sat, 24 May 2014 00:10:28 +0200 Subject: [PATCH 9/9] rework towards locator predicate --- readme.md | 2 +- ...ApprovalTest.approve_public_api.approved.txt | 2 +- ...zeWithCustomDescriptionPropertyNamesTests.cs | 4 ++-- src/Humanizer/Configuration/Configurator.cs | 17 +++++++---------- src/Humanizer/EnumHumanizeExtensions.cs | 7 +++---- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/readme.md b/readme.md index e6aead3f4..d294af357 100644 --- a/readme.md +++ b/readme.md @@ -188,7 +188,7 @@ This is to help with platforms with missing `DescriptionAttribute` and also for You can even configure the name of the property of attibute to use as description. -`Configurator.EnumDescriptionPropertyNames[typeof(EnumUnderTest)] = "Info"` +`Configurator.EnumDescriptionPropertyLocator = p => p.Name == "Info"` Hopefully this will help avoid littering enums with unnecessary attributes! diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt index 08b5fab23..15a48a6d2 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt @@ -75,7 +75,7 @@ public class Configurator { public Humanizer.Configuration.LocaliserRegistry CollectionFormatters { get; } public Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } - public System.Collections.Generic.IDictionary EnumDescriptionPropertyNames { get; } + public System.Func EnumDescriptionPropertyLocator { get; set; } public Humanizer.Configuration.LocaliserRegistry Formatters { get; } public Humanizer.Configuration.LocaliserRegistry NumberToWordsConverters { get; } public Humanizer.Configuration.LocaliserRegistry Ordinalizers { get; } diff --git a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs index 86c664564..e2cd6a395 100644 --- a/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs +++ b/src/Humanizer.Tests/EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs @@ -10,14 +10,14 @@ public class EnumHumanizeWithCustomDescriptionPropertyNamesTests : IDisposable { public EnumHumanizeWithCustomDescriptionPropertyNamesTests() { - Configurator.EnumDescriptionPropertyNames[typeof (EnumUnderTest)] = "Info"; + Configurator.EnumDescriptionPropertyLocator = p => p.Name == "Info"; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "This is a test only class, and doesn't need a 'proper' IDisposable implementation.")] public void Dispose() { - Configurator.EnumDescriptionPropertyNames.Remove(typeof (EnumUnderTest)); + Configurator.EnumDescriptionPropertyLocator = null; } [Fact] diff --git a/src/Humanizer/Configuration/Configurator.cs b/src/Humanizer/Configuration/Configurator.cs index 4c2b41c8a..af94e3c22 100644 --- a/src/Humanizer/Configuration/Configurator.cs +++ b/src/Humanizer/Configuration/Configurator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using Humanizer.DateTimeHumanizeStrategy; using Humanizer.Localisation.Formatters; using Humanizer.Localisation.NumberToWords; @@ -101,19 +102,15 @@ public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy set { _dateTimeHumanizeStrategy = value; } } - private const string DefaultEnumDescriptionPropertyName = "Description"; - private static readonly Dictionary _enumDescriptionPropertyNames = new Dictionary(); - internal static string EnumDescriptionPropertyNameFor(Type type) - { - string result = _enumDescriptionPropertyNames.TryGetValue(type, out result) ? result : null; - return result ?? DefaultEnumDescriptionPropertyName; - } + private static readonly Func DefaultEnumDescriptionPropertyLocator = p => p.Name == "Description"; + private static Func _enumDescriptionPropertyLocator = DefaultEnumDescriptionPropertyLocator; /// - /// The registry of custom attribute property names for Enum.Humanize + /// A predicate function for description property of attribute to use for Enum.Humanize /// - public static IDictionary EnumDescriptionPropertyNames + public static Func EnumDescriptionPropertyLocator { - get { return _enumDescriptionPropertyNames; } + get { return _enumDescriptionPropertyLocator; } + set { _enumDescriptionPropertyLocator = value ?? DefaultEnumDescriptionPropertyLocator; } } } } diff --git a/src/Humanizer/EnumHumanizeExtensions.cs b/src/Humanizer/EnumHumanizeExtensions.cs index a143a377f..f7cb3d584 100644 --- a/src/Humanizer/EnumHumanizeExtensions.cs +++ b/src/Humanizer/EnumHumanizeExtensions.cs @@ -25,8 +25,7 @@ public static string Humanize(this Enum input) if (memInfo.Length > 0) { - var propertyName = Configurator.EnumDescriptionPropertyNameFor(type); - var customDescription = GetCustomDescription(memInfo[0], propertyName); + var customDescription = GetCustomDescription(memInfo[0]); if (customDescription != null) return customDescription; @@ -36,7 +35,7 @@ public static string Humanize(this Enum input) } // I had to add this method because PCL doesn't have DescriptionAttribute & I didn't want two versions of the code & thus the reflection - private static string GetCustomDescription(MemberInfo memberInfo, string propertyName) + private static string GetCustomDescription(MemberInfo memberInfo) { var attrs = memberInfo.GetCustomAttributes(true); @@ -46,7 +45,7 @@ private static string GetCustomDescription(MemberInfo memberInfo, string propert var descriptionProperty = attrType.GetProperties() .Where(StringTypedProperty) - .FirstOrDefault(p => p.Name == propertyName); + .FirstOrDefault(Configurator.EnumDescriptionPropertyLocator); if (descriptionProperty != null) return descriptionProperty.GetValue(attr, null).ToString(); }