From a5c7e7295ac2e3c889d347d0285794266797db87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Sat, 18 Aug 2018 00:27:56 +0200 Subject: [PATCH] Implement System.ComponentModel.VersionConverter (#28516) * Implement System.ComponentModel.VersionConverter VersionConverter is a new System.ComponentModel.TypeConverter subclass that handle conversions between string and System.Version. * Fix tests build for VersionTypeConverter * Use Version.Parse() instead of new Version() to save an allocation * Add test data to ensure that the version string is trimmed * Call base class implementation instead of explicitly throwing * Always throw a FormatException when a version string is invalid --- .../System.ComponentModel.TypeConverter.cs | 9 ++ ...System.ComponentModel.TypeConverter.csproj | 1 + .../ReflectTypeDescriptionProvider.cs | 1 + .../System/ComponentModel/VersionConverter.cs | 94 +++++++++++++++++++ .../Performance/Perf.TypeDescriptorTests.cs | 1 + ....ComponentModel.TypeConverter.Tests.csproj | 2 + .../tests/TypeDescriptorTests.cs | 91 +++++++++--------- .../tests/TypeDescriptorTests.netcoreapp.cs | 16 ++++ .../tests/VersionConverterTests.cs | 55 +++++++++++ 9 files changed, 221 insertions(+), 49 deletions(-) create mode 100644 src/System.ComponentModel.TypeConverter/src/System/ComponentModel/VersionConverter.cs create mode 100644 src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.netcoreapp.cs create mode 100644 src/System.ComponentModel.TypeConverter/tests/VersionConverterTests.cs diff --git a/src/System.ComponentModel.TypeConverter/ref/System.ComponentModel.TypeConverter.cs b/src/System.ComponentModel.TypeConverter/ref/System.ComponentModel.TypeConverter.cs index 63aadacc5443..473cd7de23a5 100644 --- a/src/System.ComponentModel.TypeConverter/ref/System.ComponentModel.TypeConverter.cs +++ b/src/System.ComponentModel.TypeConverter/ref/System.ComponentModel.TypeConverter.cs @@ -499,6 +499,15 @@ public TimeSpanConverter() { } public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { throw null; } public override object ConvertTo(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, System.Type destinationType) { throw null; } } + public partial class VersionConverter : System.ComponentModel.TypeConverter + { + public VersionConverter() { } + public override bool CanConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Type sourceType) { throw null; } + public override bool CanConvertTo(System.ComponentModel.ITypeDescriptorContext context, System.Type destinationType) { throw null; } + public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { throw null; } + public override object ConvertTo(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, System.Type destinationType) { throw null; } + public override bool IsValid(System.ComponentModel.ITypeDescriptorContext context, object value) { throw null; } + } public partial class TypeConverter { public TypeConverter() { } diff --git a/src/System.ComponentModel.TypeConverter/src/System.ComponentModel.TypeConverter.csproj b/src/System.ComponentModel.TypeConverter/src/System.ComponentModel.TypeConverter.csproj index ec975148521b..9d39c46a76dc 100644 --- a/src/System.ComponentModel.TypeConverter/src/System.ComponentModel.TypeConverter.csproj +++ b/src/System.ComponentModel.TypeConverter/src/System.ComponentModel.TypeConverter.csproj @@ -40,6 +40,7 @@ + diff --git a/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs b/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs index 0a74b5c05d0b..3cf183b87e26 100644 --- a/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs +++ b/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs @@ -126,6 +126,7 @@ internal ReflectTypeDescriptionProvider() [typeof(TimeSpan)] = typeof(TimeSpanConverter), [typeof(Guid)] = typeof(GuidConverter), [typeof(Uri)] = typeof(UriTypeConverter), + [typeof(Version)] = typeof(VersionConverter), [typeof(Color)] = typeof(ColorConverter), [typeof(Point)] = typeof(PointConverter), [typeof(Rectangle)] = typeof(RectangleConverter), diff --git a/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/VersionConverter.cs b/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/VersionConverter.cs new file mode 100644 index 000000000000..23852ab6fa42 --- /dev/null +++ b/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/VersionConverter.cs @@ -0,0 +1,94 @@ +// 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. + +using System.Globalization; + +namespace System.ComponentModel +{ + /// + /// Provides a type converter to convert Version objects to and + /// from various other representations. + /// + public class VersionConverter : TypeConverter + { + /// + /// Gets a value indicating whether this converter can convert an object in the + /// given source type to a Version. + /// + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == null) + throw new ArgumentNullException(nameof(sourceType)); + + return sourceType == typeof(string) || sourceType == typeof(Version); + } + + /// + /// Gets a value indicating whether this converter can + /// convert an object to the given destination type using the context. + /// + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(string) || destinationType == typeof(Version); + } + + /// + /// Converts the given object to a Version. + /// + /// is not a valid version string + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string versionString) + { + try + { + return Version.Parse(versionString); + } + catch (Exception e) + { + throw new FormatException(SR.Format(SR.ConvertInvalidPrimitive, versionString, nameof(Version)), e); + } + } + + if (value is Version version) + { + return new Version(version.Major, version.Minor, version.Build, version.Revision); // return new instance + } + + return base.ConvertFrom(context, culture, value); + } + + /// + /// Converts the given value object to + /// the specified destination type using the specified context and arguments. + /// + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == null) + { + throw new ArgumentNullException(nameof(destinationType)); + } + + if (value is Version version) + { + if (destinationType == typeof(string)) + return version.ToString(); + + if (destinationType == typeof(Version)) + return new Version(version.Major, version.Minor, version.Build, version.Revision); + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + public override bool IsValid(ITypeDescriptorContext context, object value) + { + if (value is string version) + { + return Version.TryParse(version, out Version _); + } + return value is Version; + } + } +} diff --git a/src/System.ComponentModel.TypeConverter/tests/Performance/Perf.TypeDescriptorTests.cs b/src/System.ComponentModel.TypeConverter/tests/Performance/Perf.TypeDescriptorTests.cs index 45353a8dde5b..af1187c2a4ca 100644 --- a/src/System.ComponentModel.TypeConverter/tests/Performance/Perf.TypeDescriptorTests.cs +++ b/src/System.ComponentModel.TypeConverter/tests/Performance/Perf.TypeDescriptorTests.cs @@ -45,6 +45,7 @@ public class Perf_TypeDescriptorTests [InlineData(typeof(ClassIBase), typeof(IBaseConverter))] [InlineData(typeof(ClassIDerived), typeof(IBaseConverter))] [InlineData(typeof(Uri), typeof(UriTypeConverter))] + [InlineData(typeof(Version), typeof(VersionConverter))] public static void GetConverter(Type typeToConvert, Type expectedConverter) { const int innerIterations = 100; diff --git a/src/System.ComponentModel.TypeConverter/tests/System.ComponentModel.TypeConverter.Tests.csproj b/src/System.ComponentModel.TypeConverter/tests/System.ComponentModel.TypeConverter.Tests.csproj index 70be8e528f6a..2d00902e8471 100644 --- a/src/System.ComponentModel.TypeConverter/tests/System.ComponentModel.TypeConverter.Tests.csproj +++ b/src/System.ComponentModel.TypeConverter/tests/System.ComponentModel.TypeConverter.Tests.csproj @@ -121,11 +121,13 @@ + + diff --git a/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.cs b/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.cs index 86e5bcd6a29d..e31ae67ac9dc 100644 --- a/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.cs +++ b/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.cs @@ -8,7 +8,7 @@ namespace System.ComponentModel.Tests { - public class TypeDescriptorTests + public partial class TypeDescriptorTests { [Fact] public void AddAndRemoveProvider() @@ -66,16 +66,48 @@ public void GetAssociationReturnsExpectedObject() Assert.Equal(secondaryObject, associatedObject); } - [Fact] - public static void GetConverter() + [Theory] + [InlineData(typeof(bool), typeof(BooleanConverter))] + [InlineData(typeof(byte), typeof(ByteConverter))] + [InlineData(typeof(sbyte), typeof(SByteConverter))] + [InlineData(typeof(char), typeof(CharConverter))] + [InlineData(typeof(double), typeof(DoubleConverter))] + [InlineData(typeof(string), typeof(StringConverter))] + [InlineData(typeof(short), typeof(Int16Converter))] + [InlineData(typeof(int), typeof(Int32Converter))] + [InlineData(typeof(long), typeof(Int64Converter))] + [InlineData(typeof(float), typeof(SingleConverter))] + [InlineData(typeof(ushort), typeof(UInt16Converter))] + [InlineData(typeof(uint), typeof(UInt32Converter))] + [InlineData(typeof(ulong), typeof(UInt64Converter))] + [InlineData(typeof(object), typeof(TypeConverter))] + [InlineData(typeof(void), typeof(TypeConverter))] + [InlineData(typeof(DateTime), typeof(DateTimeConverter))] + [InlineData(typeof(DateTimeOffset), typeof(DateTimeOffsetConverter))] + [InlineData(typeof(decimal), typeof(DecimalConverter))] + [InlineData(typeof(TimeSpan), typeof(TimeSpanConverter))] + [InlineData(typeof(Guid), typeof(GuidConverter))] + [InlineData(typeof(Array), typeof(ArrayConverter))] + [InlineData(typeof(ICollection), typeof(CollectionConverter))] + [InlineData(typeof(Enum), typeof(EnumConverter))] + [InlineData(typeof(SomeEnum), typeof(EnumConverter))] + [InlineData(typeof(SomeValueType?), typeof(NullableConverter))] + [InlineData(typeof(int?), typeof(NullableConverter))] + [InlineData(typeof(ClassWithNoConverter), typeof(TypeConverter))] + [InlineData(typeof(BaseClass), typeof(BaseClassConverter))] + [InlineData(typeof(DerivedClass), typeof(DerivedClassConverter))] + [InlineData(typeof(IBase), typeof(IBaseConverter))] + [InlineData(typeof(IDerived), typeof(IBaseConverter))] + [InlineData(typeof(ClassIBase), typeof(IBaseConverter))] + [InlineData(typeof(ClassIDerived), typeof(IBaseConverter))] + [InlineData(typeof(Uri), typeof(UriTypeConverter))] + [InlineData(typeof(CultureInfo), typeof(CultureInfoConverter))] + public static void GetConverter(Type targetType, Type resultConverterType) { - foreach (Tuple pair in s_typesWithConverters) - { - TypeConverter converter = TypeDescriptor.GetConverter(pair.Item1); - Assert.NotNull(converter); - Assert.Equal(pair.Item2, converter.GetType()); - Assert.True(converter.CanConvertTo(typeof(string))); - } + TypeConverter converter = TypeDescriptor.GetConverter(targetType); + Assert.NotNull(converter); + Assert.Equal(resultConverterType, converter.GetType()); + Assert.True(converter.CanConvertTo(typeof(string))); } [Fact] @@ -239,44 +271,5 @@ class FooBarDerived : FooBarBase [Description("Derived")] public override int Value { get; set; } } - - private static Tuple[] s_typesWithConverters = - { - new Tuple (typeof(bool), typeof(BooleanConverter)), - new Tuple (typeof(byte), typeof(ByteConverter)), - new Tuple (typeof(sbyte), typeof(SByteConverter)), - new Tuple (typeof(char), typeof(CharConverter)), - new Tuple (typeof(double), typeof(DoubleConverter)), - new Tuple (typeof(string), typeof(StringConverter)), - new Tuple (typeof(short), typeof(Int16Converter)), - new Tuple (typeof(int), typeof(Int32Converter)), - new Tuple (typeof(long), typeof(Int64Converter)), - new Tuple (typeof(float), typeof(SingleConverter)), - new Tuple (typeof(ushort), typeof(UInt16Converter)), - new Tuple (typeof(uint), typeof(UInt32Converter)), - new Tuple (typeof(ulong), typeof(UInt64Converter)), - new Tuple (typeof(object), typeof(TypeConverter)), - new Tuple (typeof(void), typeof(TypeConverter)), - new Tuple (typeof(DateTime), typeof(DateTimeConverter)), - new Tuple (typeof(DateTimeOffset), typeof(DateTimeOffsetConverter)), - new Tuple (typeof(decimal), typeof(DecimalConverter)), - new Tuple (typeof(TimeSpan), typeof(TimeSpanConverter)), - new Tuple (typeof(Guid), typeof(GuidConverter)), - new Tuple (typeof(Array), typeof(ArrayConverter)), - new Tuple (typeof(ICollection), typeof(CollectionConverter)), - new Tuple (typeof(Enum), typeof(EnumConverter)), - new Tuple (typeof(SomeEnum), typeof(EnumConverter)), - new Tuple (typeof(SomeValueType?), typeof(NullableConverter)), - new Tuple (typeof(int?), typeof(NullableConverter)), - new Tuple (typeof(ClassWithNoConverter), typeof(TypeConverter)), - new Tuple (typeof(BaseClass), typeof(BaseClassConverter)), - new Tuple (typeof(DerivedClass), typeof(DerivedClassConverter)), - new Tuple (typeof(IBase), typeof(IBaseConverter)), - new Tuple (typeof(IDerived), typeof(IBaseConverter)), - new Tuple (typeof(ClassIBase), typeof(IBaseConverter)), - new Tuple (typeof(ClassIDerived), typeof(IBaseConverter)), - new Tuple (typeof(Uri), typeof(UriTypeConverter)), - new Tuple (typeof(CultureInfo), typeof(CultureInfoConverter)) - }; } } diff --git a/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.netcoreapp.cs b/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.netcoreapp.cs new file mode 100644 index 000000000000..397c94d26f2e --- /dev/null +++ b/src/System.ComponentModel.TypeConverter/tests/TypeDescriptorTests.netcoreapp.cs @@ -0,0 +1,16 @@ +// 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. + +using Xunit; + +namespace System.ComponentModel.Tests +{ + public partial class TypeDescriptorTests + { + [Theory] + [InlineData(typeof(Version), typeof(VersionConverter))] + public static void GetConverter_NetCoreApp(Type targetType, Type resultConverterType) => + GetConverter(targetType, resultConverterType); + } +} diff --git a/src/System.ComponentModel.TypeConverter/tests/VersionConverterTests.cs b/src/System.ComponentModel.TypeConverter/tests/VersionConverterTests.cs new file mode 100644 index 000000000000..9e8ec3c905f6 --- /dev/null +++ b/src/System.ComponentModel.TypeConverter/tests/VersionConverterTests.cs @@ -0,0 +1,55 @@ +// 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. + +using Xunit; + +namespace System.ComponentModel.Tests +{ + public class VersionConverterTests : ConverterTestBase + { + private static VersionConverter s_converter = new VersionConverter(); + + [Fact] + public static void CanConvertFrom_WithContext() + { + CanConvertFrom_WithContext(new object[2, 2] + { + { typeof(string), true }, + { typeof(Version), true } + }, + VersionConverterTests.s_converter); + } + + [Fact] + public static void ConvertFrom_WithContext() + { + ConvertFrom_WithContext(new object[4, 3] + { + {"1.2", new Version(1, 2), null}, + {"1.2.3", new Version(1, 2, 3), null}, + {"1.2.3.4", new Version(1, 2, 3, 4), null}, + {" 1.2.3.4 ", new Version(1, 2, 3, 4), null} + }, + VersionConverterTests.s_converter); + } + + [Fact] + public static void ConvertFromNull_WithContext_ThrowsNotSupportedException() + { + Assert.Throws( + () => VersionConverterTests.s_converter.ConvertFrom(TypeConverterTests.s_context, null, null)); + } + + [Theory] + [InlineData("")] + [InlineData("1")] + [InlineData("1.-2")] + [InlineData("1.9999999999")] + public static void ConvertFromInvalidVersion_WithContext_ThrowsFormatException(string version) + { + Assert.Throws( + () => VersionConverterTests.s_converter.ConvertFrom(TypeConverterTests.s_context, null, version)); + } + } +}