diff --git a/src/Controls/src/Core.Design/AttributeTableBuilder.cs b/src/Controls/src/Core.Design/AttributeTableBuilder.cs index 3d5a7b536274..666a1f782fbe 100644 --- a/src/Controls/src/Core.Design/AttributeTableBuilder.cs +++ b/src/Controls/src/Core.Design/AttributeTableBuilder.cs @@ -43,8 +43,7 @@ public AttributeTableBuilder() new TypeConverterAttribute(typeof(ItemsLayoutDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.InputView", "Keyboard", - new TypeConverterAttribute(typeof(KeyboardDesignTypeConverter)), - new TypeConverterAttribute(typeof(StringConverter))); + new TypeConverterAttribute(typeof(KeyboardDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.EntryCell", "Keyboard", new TypeConverterAttribute(typeof(KeyboardDesignTypeConverter))); @@ -67,37 +66,51 @@ public AttributeTableBuilder() //new System.Windows.Markup.MarkupExtensionReturnTypeAttribute (), ); + // Type level attributes AddTypeAttributes("Microsoft.Maui.Controls.Brush", new TypeConverterAttribute(typeof(ColorDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Graphics.Color", new TypeConverterAttribute(typeof(ColorDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.GridLength", new TypeConverterAttribute(typeof(GridLengthDesignTypeConverter))); - + AddTypeAttributes("Microsoft.Maui.Controls.Button+ButtonContentLayout", new TypeConverterAttribute(typeof(ButtonContentDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Controls.Compatibility.Constraint", new TypeConverterAttribute(typeof(ConstraintDesignTypeConverter))); AddTypeAttributes("Microsoft.Maui.Controls.ConstraintExpression", new MarkupExtensionReturnTypeAttribute()); + AddTypeAttributes("Microsoft.Maui.Controls.ImageSource", new TypeConverterAttribute(typeof(ImageSourceDesignTypeConverter))); AddTypeAttributes("Microsoft.Maui.Controls.LayoutOptions", new TypeConverterAttribute(typeof(LayoutOptionsDesignTypeConverter))); AddTypeAttributes("Microsoft.Maui.Controls.LinearItemsLayout", new TypeConverterAttribute(typeof(LinearItemsLayoutDesignTypeConverter))); AddTypeAttributes("Microsoft.Maui.Controls.ResourcesChangedEventArgs", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.CornerRadius", new TypeConverterAttribute(typeof(CornerRadiusDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Graphics.Color", new TypeConverterAttribute(typeof(ColorDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Graphics.Point", new TypeConverterAttribute(typeof(PointTypeDesignConverter))); + AddTypeAttributes("Microsoft.Maui.Graphics.Rect", new TypeConverterAttribute(typeof(RectTypeDesignConverter))); + AddTypeAttributes("Microsoft.Maui.GridLength", new TypeConverterAttribute(typeof(GridLengthDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexAlignContent", new TypeConverterAttribute(typeof(FlexAlignContentDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexAlignItems", new TypeConverterAttribute(typeof(FlexAlignItemsDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexAlignSelf", new TypeConverterAttribute(typeof(FlexAlignSelfDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexBasis", new TypeConverterAttribute(typeof(FlexBasisDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexDirection", new TypeConverterAttribute(typeof(FlexDirectionDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexJustify", new TypeConverterAttribute(typeof(FlexJustifyDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Layouts.FlexWrap", new TypeConverterAttribute(typeof(FlexWrapDesignTypeConverter))); + AddTypeAttributes("Microsoft.Maui.Thickness", new TypeConverterAttribute(typeof(ThicknessTypeDesignConverter))); + // Property level attributes + AddMemberAttributes("Microsoft.Maui.Controls.AbsoluteLayout", "LayoutBounds", new TypeConverterAttribute(typeof(BoundsDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.Button", "ContentLayout", new TypeConverterAttribute(typeof(ButtonContentDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.Button", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.Compatibility.AbsoluteLayout", "LayoutBounds", new TypeConverterAttribute(typeof(BoundsDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.DatePicker", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.Editor", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.Entry", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.Grid", "ColumnDefinitions", new TypeConverterAttribute(typeof(GridLengthCollectionDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.Grid", "RowDefinitions", new TypeConverterAttribute(typeof(GridLengthCollectionDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.Image", "Source", new TypeConverterAttribute(typeof(ImageSourceDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.ImageButton", "Source", new TypeConverterAttribute(typeof(ImageSourceDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.ImageCell", "ImageSource", new TypeConverterAttribute(typeof(ImageSourceDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.Label", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.Picker", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); - AddMemberAttributes("Microsoft.Maui.Controls.SearchBar", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); - AddMemberAttributes("Microsoft.Maui.Controls.TimePicker", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.RadioButton", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.SearchBar", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.SearchHandler", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.Shell", "FlyoutBackgroundImage", new TypeConverterAttribute(typeof(ImageSourceDesignTypeConverter))); AddMemberAttributes("Microsoft.Maui.Controls.Span", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); - - AddMemberAttributes("Microsoft.Maui.Controls.Grid", "RowDefinitions", new TypeConverterAttribute(typeof(GridLengthCollectionDesignTypeConverter))); - AddMemberAttributes("Microsoft.Maui.Controls.Grid", "ColumnDefinitions", new TypeConverterAttribute(typeof(GridLengthCollectionDesignTypeConverter))); - - AddTypeAttributes("Microsoft.Maui.Layouts.FlexJustify", new TypeConverterAttribute(typeof(FlexJustifyDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Layouts.FlexDirection", new TypeConverterAttribute(typeof(FlexDirectionDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Layouts.FlexAlignContent", new TypeConverterAttribute(typeof(FlexAlignContentDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Layouts.FlexAlignItems", new TypeConverterAttribute(typeof(FlexAlignItemsDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Layouts.FlexAlignSelf", new TypeConverterAttribute(typeof(FlexAlignSelfDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Layouts.FlexWrap", new TypeConverterAttribute(typeof(FlexWrapDesignTypeConverter))); - AddTypeAttributes("Microsoft.Maui.Layouts.FlexBasis", new TypeConverterAttribute(typeof(FlexBasisDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.TimePicker", "FontSize", new TypeConverterAttribute(typeof(FontSizeDesignTypeConverter))); + AddMemberAttributes("Microsoft.Maui.Controls.VisualElement", "IsVisible", new TypeConverterAttribute(typeof(VisibilityDesignTypeConverter))); } private void AddTypeAttributes(string typeName, params Attribute[] attribs) diff --git a/src/Controls/src/Core.Design/BoundsDesignTypeConverter.cs b/src/Controls/src/Core.Design/BoundsDesignTypeConverter.cs new file mode 100644 index 000000000000..1e0076d6feb8 --- /dev/null +++ b/src/Controls/src/Core.Design/BoundsDesignTypeConverter.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Microsoft.Maui.Controls.Design +{ + public class BoundsDesignTypeConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH BoundsTypeConverter.ConvertFrom + string strValue = value?.ToString(); + if (string.IsNullOrEmpty(strValue)) + return false; + + string[] xywh = strValue.Split(','); + bool hasX, hasY, hasW, hasH; + + hasX = (xywh.Length == 2 || xywh.Length == 4) && double.TryParse(xywh[0], NumberStyles.Number, CultureInfo.InvariantCulture, out _); + hasY = (xywh.Length == 2 || xywh.Length == 4) && double.TryParse(xywh[1], NumberStyles.Number, CultureInfo.InvariantCulture, out _); + hasW = xywh.Length == 4 && double.TryParse(xywh[2], NumberStyles.Number, CultureInfo.InvariantCulture, out _); + hasH = xywh.Length == 4 && double.TryParse(xywh[3], NumberStyles.Number, CultureInfo.InvariantCulture, out _); + if (!hasW && xywh.Length == 4 && string.Compare("AutoSize", xywh[2].Trim(), StringComparison.OrdinalIgnoreCase) == 0) + { + hasW = true; + } + + if (!hasH && xywh.Length == 4 && string.Compare("AutoSize", xywh[3].Trim(), StringComparison.OrdinalIgnoreCase) == 0) + { + hasH = true; + } + + if (hasX && hasY && xywh.Length == 2) + return true; + if (hasX && hasY && hasW && hasH && xywh.Length == 4) + return true; + + return false; + } + } +} diff --git a/src/Controls/src/Core.Design/ButtonContentDesignTypeConverter.cs b/src/Controls/src/Core.Design/ButtonContentDesignTypeConverter.cs new file mode 100644 index 000000000000..f4fd3bac69f8 --- /dev/null +++ b/src/Controls/src/Core.Design/ButtonContentDesignTypeConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel; + +namespace Microsoft.Maui.Controls.Design +{ + public class ButtonContentDesignTypeConverter : StringConverter + { + private static readonly char[] Separators = { ',' }; + + // MUST MATCH values of ButtonContentConverter.ImagePosition. NOTE that we use enum rather than strings + // to better match ButtonContentConverter. First, Enum.Parse will accept int values like 1 or 20. Second, + // values passed to converter contain numbers and enum values, i.e. there is a possibility that we might + // need to handle ints in place of enums. ButtonContentConverter does not properly handle corner cases. + // For example, it does not trim incoming strings, so " 15" will be converted as + // ButtonContentLayout((ImagePorition)15, DefaultSpacing/*10*/) + private enum ImagePosition { Left, Top, Right, Bottom }; + + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH ButtonContentConverter.ConvertFrom + string stringValue = value?.ToString(); + if (string.IsNullOrEmpty(stringValue)) + return false; + + string[] parts = stringValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 2) + return false; + + if (char.IsDigit(parts[0][0])) + { + // Examples: "5" or "10, Top" + if (!double.TryParse(parts[0], out _)) + return false; // bogus number, e.g. 5a + if (parts.Length == 2 && !Enum.TryParse(parts[1], true, out ImagePosition _)) + return false; // bogus position, e.g. "Hello" + } + else + { + // Examples: "Right" or "Bottom, 5" + if (!Enum.TryParse(parts[0], true, out ImagePosition _)) + return false; // bogus position, e.g. "Hello" + if (parts.Length == 2 && !double.TryParse(parts[1], out _)) + return false; // bogus number, e.g. 5a + } + + return true; + } + } +} diff --git a/src/Controls/src/Core.Design/ConstraintDesignTypeConverter.cs b/src/Controls/src/Core.Design/ConstraintDesignTypeConverter.cs new file mode 100644 index 000000000000..ff3ebd400354 --- /dev/null +++ b/src/Controls/src/Core.Design/ConstraintDesignTypeConverter.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using System.Globalization; + +namespace Microsoft.Maui.Controls.Design +{ + public class ConstraintDesignTypeConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH ConstraintTypeConverter.ConvertFrom + var strValue = value?.ToString(); + return (strValue != null && double.TryParse(strValue, NumberStyles.Number, CultureInfo.InvariantCulture, out _)); + } + } +} diff --git a/src/Controls/src/Core.Design/Controls.Core.Design.csproj b/src/Controls/src/Core.Design/Controls.Core.Design.csproj index 4a0e896da57d..55684069f45f 100644 --- a/src/Controls/src/Core.Design/Controls.Core.Design.csproj +++ b/src/Controls/src/Core.Design/Controls.Core.Design.csproj @@ -8,25 +8,37 @@ <_MauiDesignDllBuild Condition=" '$(OS)' != 'Unix' ">True - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controls/src/Core.Design/CornerRadiusDesignTypeConverter.cs b/src/Controls/src/Core.Design/CornerRadiusDesignTypeConverter.cs new file mode 100644 index 000000000000..02c001339bf8 --- /dev/null +++ b/src/Controls/src/Core.Design/CornerRadiusDesignTypeConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using Controls.Core.Design; + +namespace Microsoft.Maui.Controls.Design +{ + public class CornerRadiusDesignTypeConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH CornerRadiusTypeConverter.ConvertFrom + var strValue = value?.ToString(); + if (strValue != null) + { + if (strValue.IndexOf(",", StringComparison.Ordinal) != -1) + { + var parts = strValue.Split(','); + + // Example: "1,2,3,4" + if (parts.Length == 4) + { + foreach (string part in parts) + { + if (!double.TryParse(part, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) + return false; + } + + return true; + } + + // Example: "1,a,b". CornerRadiusTypeConverter has unusual behavior + // for 2 or 3 token string. We match its behavior here + if (parts.Length < 4) + return double.TryParse(parts[0], NumberStyles.Number, CultureInfo.InvariantCulture, out _); + } + else + { + // Example: "1 2 3 4". Any count of numbers between 1 and 4 is valid. + // Note that CornerRadiusTypeConverter is sensitive to spaces, e.g. + // "1 2" is valid but "1 2" is not. We match its behavior here. + return DesignTypeConverterHelper.TryParseNumbers(strValue.Trim(), ' ', maxCount: 4) is int; + } + } + + return false; + } + } +} diff --git a/src/Controls/src/Core.Design/DesignTypeConverterHelper.cs b/src/Controls/src/Core.Design/DesignTypeConverterHelper.cs new file mode 100644 index 000000000000..1c4f2120cddc --- /dev/null +++ b/src/Controls/src/Core.Design/DesignTypeConverterHelper.cs @@ -0,0 +1,28 @@ +using System.Globalization; + +namespace Controls.Core.Design +{ + internal static class DesignTypeConverterHelper + { + /// + /// Returns count of numbers in the string. Returns null if some of the values are invalid or total count exceeds max count. + /// + public static int? TryParseNumbers(string numberCollection, char separator, int maxCount) + { + // Examples: "1,2" or "1 2 3 4". + if (string.IsNullOrEmpty(numberCollection)) + return null; + + string[] parts = numberCollection.Split(separator); + if (parts.Length > maxCount) + return null; // too many numbers + + foreach (string part in parts) + { + if (!double.TryParse(part, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) + return null; // invalid number found + } + return parts.Length; // all numbers are valid + } + } +} diff --git a/src/Controls/src/Core.Design/ImageSourceDesignTypeConverter.cs b/src/Controls/src/Core.Design/ImageSourceDesignTypeConverter.cs new file mode 100644 index 000000000000..58af35904691 --- /dev/null +++ b/src/Controls/src/Core.Design/ImageSourceDesignTypeConverter.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel; + +namespace Microsoft.Maui.Controls.Design +{ + public class ImageSourceDesignTypeConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH ImageSourceConverter.ConvertFrom + if (value?.ToString() is string strValue) + return Uri.TryCreate(strValue, UriKind.Absolute, out Uri _); + + return false; + } + } +} diff --git a/src/Controls/src/Core.Design/PointTypeDesignConverter.cs b/src/Controls/src/Core.Design/PointTypeDesignConverter.cs new file mode 100644 index 000000000000..a327b5236229 --- /dev/null +++ b/src/Controls/src/Core.Design/PointTypeDesignConverter.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using Controls.Core.Design; + +namespace Microsoft.Maui.Controls.Design +{ + public class PointTypeDesignConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH Point.TryParse + int? count = DesignTypeConverterHelper.TryParseNumbers(value?.ToString(), ',', 2); + return count == 2; + } + } +} diff --git a/src/Controls/src/Core.Design/RectTypeDesignConverter.cs b/src/Controls/src/Core.Design/RectTypeDesignConverter.cs new file mode 100644 index 000000000000..3cec67ef411c --- /dev/null +++ b/src/Controls/src/Core.Design/RectTypeDesignConverter.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using Controls.Core.Design; + +namespace Microsoft.Maui.Controls.Design +{ + public class RectTypeDesignConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH Rect.TryParse + int? count = DesignTypeConverterHelper.TryParseNumbers(value?.ToString(), ',', 4); + return count == 4; + } + } +} diff --git a/src/Controls/src/Core.Design/ThicknessTypeDesignConverter.cs b/src/Controls/src/Core.Design/ThicknessTypeDesignConverter.cs new file mode 100644 index 000000000000..1da72e03f2b2 --- /dev/null +++ b/src/Controls/src/Core.Design/ThicknessTypeDesignConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel; +using Controls.Core.Design; + +namespace Microsoft.Maui.Controls.Design +{ + public class ThicknessTypeDesignConverter : StringConverter + { + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH ThicknessTypeConverter.ConvertFrom + string strValue = value?.ToString()?.Trim(); + if (string.IsNullOrEmpty(strValue)) + return false; + + if (strValue.IndexOf(",", StringComparison.Ordinal) != -1) + { + int? count = DesignTypeConverterHelper.TryParseNumbers(value?.ToString(), ',', maxCount: 4); + return count == 2 || count == 4; + } + else + { + // Example: "1 2 3 4". Any count of numbers between 1 and 4 is valid. + // Note that ThicknessTypeConverter is sensitive to spaces, e.g. + // "1 2" is valid but "1 2" is not. We match its behavior here. + return DesignTypeConverterHelper.TryParseNumbers(strValue.Trim(), ' ', maxCount: 4) is int; + } + } + } +} diff --git a/src/Controls/src/Core.Design/VisibilityDesignTypeConverter.cs b/src/Controls/src/Core.Design/VisibilityDesignTypeConverter.cs new file mode 100644 index 000000000000..af9305c3e59f --- /dev/null +++ b/src/Controls/src/Core.Design/VisibilityDesignTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Microsoft.Maui.Controls.Design +{ + public class VisibilityDesignTypeConverter : StringConverter + { + private static readonly string[] validValues = ["Collapse", "Hidden", bool.FalseString, bool.TrueString, "Visible"]; + private static readonly HashSet supportedValues = new HashSet(validValues, StringComparer.OrdinalIgnoreCase); + private static readonly StandardValuesCollection standardValues = new StandardValuesCollection(validValues); + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) => true; + override public bool GetStandardValuesSupported(ITypeDescriptorContext context) => true; + override public StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) => standardValues; + public override bool IsValid(ITypeDescriptorContext context, object value) + { + // MUST MATCH VisibilityConverter.ConvertFrom + if (value?.ToString()?.Trim() is string strValue) + return supportedValues.Contains(strValue); + + return false; + } + } +} diff --git a/src/Controls/src/Core/Button/Button.cs b/src/Controls/src/Core/Button/Button.cs index 995c94b45a6f..8cab188ab4d2 100644 --- a/src/Controls/src/Core/Button/Button.cs +++ b/src/Controls/src/Core/Button/Button.cs @@ -577,6 +577,7 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // IMPORTANT! Update ButtonContentDesignTypeConverter.IsValid if making changes here var strValue = value?.ToString(); if (strValue == null) throw new InvalidOperationException($"Cannot convert null into {typeof(ButtonContentLayout)}"); diff --git a/src/Controls/src/Core/ImageSourceConverter.cs b/src/Controls/src/Core/ImageSourceConverter.cs index a450dafed0df..4fac2085f0d9 100644 --- a/src/Controls/src/Core/ImageSourceConverter.cs +++ b/src/Controls/src/Core/ImageSourceConverter.cs @@ -18,6 +18,7 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // IMPORTANT! Update ImageSourceDesignTypeConverter.IsValid if making changes here var strValue = value?.ToString(); if (strValue != null) return Uri.TryCreate(strValue, UriKind.Absolute, out Uri uri) && uri.Scheme != "file" ? ImageSource.FromUri(uri) : ImageSource.FromFile(strValue); diff --git a/src/Controls/src/Core/Layout/BoundsTypeConverter.cs b/src/Controls/src/Core/Layout/BoundsTypeConverter.cs index b80bb1ce7716..5fe7ab443d08 100644 --- a/src/Controls/src/Core/Layout/BoundsTypeConverter.cs +++ b/src/Controls/src/Core/Layout/BoundsTypeConverter.cs @@ -18,6 +18,7 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // IMPORTANT! Update BoundsDesignTypeConverter.IsValid if making changes here var strValue = value?.ToString(); if (strValue != null) diff --git a/src/Controls/src/Core/LegacyLayouts/ConstraintTypeConverter.cs b/src/Controls/src/Core/LegacyLayouts/ConstraintTypeConverter.cs index fd778b5cae31..c0a58f1a80d5 100644 --- a/src/Controls/src/Core/LegacyLayouts/ConstraintTypeConverter.cs +++ b/src/Controls/src/Core/LegacyLayouts/ConstraintTypeConverter.cs @@ -16,6 +16,7 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // IMPORTANT! Update ConstraintDesignTypeConverter.IsValid if making changes here var strValue = value?.ToString(); if (strValue != null && double.TryParse(strValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var size)) diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index f7f331f2a944..3b86e02b0115 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -2263,6 +2263,7 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // IMPORTANT! Update VisibilityDesignTypeConverter.IsValid if making changes here var strValue = value?.ToString()?.Trim(); if (!string.IsNullOrEmpty(strValue)) diff --git a/src/Controls/tests/Core.Design.UnitTests/BoundsDesignTypeConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/BoundsDesignTypeConverterTests.cs new file mode 100644 index 000000000000..c1a7d0d2791c --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/BoundsDesignTypeConverterTests.cs @@ -0,0 +1,41 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class BoundsDesignTypeConverterTests + { + [Theory] + [InlineData("1,2 ")] + [InlineData(" 3.1, -4.2, 5, -6")] + [InlineData(" 14,17, AutoSize, -20 ")] + [InlineData("5,6,7,AUTOSIZE")] + [InlineData("11,-12, autosize, AutoSize")] + public void BoundsDesignTypeConverter_Valid(string value) + { + BoundsDesignTypeConverter converter = new BoundsDesignTypeConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("1")] + [InlineData("2,3,4")] + [InlineData(",7,8")] + [InlineData("9,10,")] + [InlineData("11,12,13,14,15")] + [InlineData("AutoSize,AutoSize")] + [InlineData("AutoSize,AutoSize,AutoSize,AutoSize")] + public void BoundsDesignTypeConverter_Invalid(string value) + { + BoundsDesignTypeConverter converter = new BoundsDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/ButtonContentDesignTypeConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/ButtonContentDesignTypeConverterTests.cs new file mode 100644 index 000000000000..f0c76216c2e3 --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/ButtonContentDesignTypeConverterTests.cs @@ -0,0 +1,51 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class ButtonContentDesignTypeConverterTests + { + [Theory] + [InlineData("Left")] + [InlineData(" TOP ")] + [InlineData("2, right")] + [InlineData("Left, -25 ")] + [InlineData(" Bottom, .3 ")] + public void ButtonContentDesignTypeConverter_Valid_Common(string value) + { + ButtonContentDesignTypeConverter converter = new ButtonContentDesignTypeConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(",1")] + [InlineData("9,15")] + [InlineData(",10,,,Top,")] + [InlineData(" 4,,,,")] + [InlineData("Left,")] + [InlineData(" 15")] + public void ButtonContentDesignTypeConverter_Valid_Unusual(string value) + { + // ButtonContentConverter.ConvertFrom allows these cases + ButtonContentDesignTypeConverter converter = new ButtonContentDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(".2,Top")] + [InlineData("Left,Right")] + [InlineData(".3,.4")] + public void ButtonContentDesignTypeConverter_Invalid(string value) + { + ButtonContentDesignTypeConverter converter = new ButtonContentDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/ConstraintDesignTypeConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/ConstraintDesignTypeConverterTests.cs new file mode 100644 index 000000000000..0cbfbf2679de --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/ConstraintDesignTypeConverterTests.cs @@ -0,0 +1,34 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class ConstraintDesignTypeConverterTests + { + [Theory] + [InlineData("1")] + [InlineData(" -2.3")] + [InlineData(" NaN ")] + [InlineData("Infinity")] + public void ConstraintDesignTypeConverter_Valid(string value) + { + ConstraintDesignTypeConverter converter = new ConstraintDesignTypeConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("1a")] + public void ConstraintDesignTypeConverter_Invalid(string value) + { + ConstraintDesignTypeConverter converter = new ConstraintDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/Controls.Core.Design.UnitTests.csproj b/src/Controls/tests/Core.Design.UnitTests/Controls.Core.Design.UnitTests.csproj index 0d4428eb873d..6554ac7bbda4 100644 --- a/src/Controls/tests/Core.Design.UnitTests/Controls.Core.Design.UnitTests.csproj +++ b/src/Controls/tests/Core.Design.UnitTests/Controls.Core.Design.UnitTests.csproj @@ -17,9 +17,18 @@ + + + + + + + + + diff --git a/src/Controls/tests/Core.Design.UnitTests/CornerRadiusDesignTypeConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/CornerRadiusDesignTypeConverterTests.cs new file mode 100644 index 000000000000..60f24c7ab306 --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/CornerRadiusDesignTypeConverterTests.cs @@ -0,0 +1,50 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class CornerRadiusDesignTypeConverterTests + { + [Theory] + [InlineData("1.6")] + [InlineData("1, 2.7")] + [InlineData("1, 2, 3.8")] + [InlineData("1,2,3,4.9")] + [InlineData("1 2.7")] + [InlineData("1 2 3.8")] + [InlineData("1 2 3 4.9")] + public void CornerRadiusDesignTypeConverter_Valid_Common(string value) + { + CornerRadiusDesignTypeConverter converter = new CornerRadiusDesignTypeConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] // CornerRadiusTypeConverter has unusual behavior for 2 or 3 comma separated tokens; design converter matches its behavior + [InlineData("1,")] + [InlineData("2,,")] + [InlineData("3,hello")] + [InlineData("4,hello,world")] + public void CornerRadiusDesignTypeConverter_Valid_Unusual(string value) + { + CornerRadiusDesignTypeConverter converter = new CornerRadiusDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("1 2")] // CornerRadiusTypeConverter is sensitive to spaces; design converter matches its behavior + [InlineData("1,2,3,4,5")] + [InlineData("1 2 3 4 5")] + public void CornerRadiusDesignTypeConverter_Invalid(string value) + { + CornerRadiusDesignTypeConverter converter = new CornerRadiusDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/ImageSourceDesignTypeConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/ImageSourceDesignTypeConverterTests.cs new file mode 100644 index 000000000000..301f6af0bb27 --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/ImageSourceDesignTypeConverterTests.cs @@ -0,0 +1,32 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class ImageSourceDesignTypeConverterTests + { + [Theory] + [InlineData("http://consoto.com")] + [InlineData("file:///x:/logo.png")] + public void ImageSourceDesignTypeConverter_Valid(string value) + { + ImageSourceDesignTypeConverter converter = new ImageSourceDesignTypeConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("consoto.com")] + [InlineData("/app/logo.png")] + public void ImageSourceDesignTypeConverter_Invalid(string value) + { + ImageSourceDesignTypeConverter converter = new ImageSourceDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/PointTypeDesignConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/PointTypeDesignConverterTests.cs new file mode 100644 index 000000000000..ac5fe3296aff --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/PointTypeDesignConverterTests.cs @@ -0,0 +1,33 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class PointTypeDesignConverterTests + { + [Theory] + [InlineData("1,2")] + [InlineData(" -3.5, NaN")] + public void PointTypeDesignConverter_Valid(string value) + { + PointTypeDesignConverter converter = new PointTypeDesignConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("1")] + [InlineData("1,2,3")] + [InlineData("a,b")] + public void PointTypeDesignConverter_Invalid(string value) + { + VisibilityDesignTypeConverter converter = new VisibilityDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/RectTypeDesignConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/RectTypeDesignConverterTests.cs new file mode 100644 index 000000000000..15170add5b68 --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/RectTypeDesignConverterTests.cs @@ -0,0 +1,34 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class RectTypeDesignConverterTests + { + [Theory] + [InlineData("1,2,3,4")] + [InlineData(" -3.5, NaN, 7, Infinity")] + public void RectTypeDesignConverter_Valid(string value) + { + RectTypeDesignConverter converter = new RectTypeDesignConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("1")] + [InlineData("1,2")] + [InlineData("1,2,3,4,5")] + [InlineData("a,b")] + public void RectTypeDesignConverter_Invalid(string value) + { + RectTypeDesignConverter converter = new RectTypeDesignConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/ThicknessTypeDesignConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/ThicknessTypeDesignConverterTests.cs new file mode 100644 index 000000000000..c0d151b4ec62 --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/ThicknessTypeDesignConverterTests.cs @@ -0,0 +1,40 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class ThicknessTypeDesignConverterTests + { + [Theory] + [InlineData("-5")] + [InlineData("1,2")] + [InlineData("1,2, 3, 4 ")] + [InlineData("1 2")] + [InlineData("1 2 3")] + [InlineData("1 2 3 4")] + public void ThicknessTypeDesignConverter_Valid(string value) + { + ThicknessTypeDesignConverter converter = new ThicknessTypeDesignConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("1,2,3")] + [InlineData("1,2,3,4,5")] + [InlineData("1 2")] // ThicknessConverter is sensitive to spaces; design converter matches its behavior + [InlineData("1 2 3 4 5")] + [InlineData("a,b")] + public void ThicknessTypeDesignConverter_Invalid(string value) + { + ThicknessTypeDesignConverter converter = new ThicknessTypeDesignConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Controls/tests/Core.Design.UnitTests/VisibilityDesignTypeConverterTests.cs b/src/Controls/tests/Core.Design.UnitTests/VisibilityDesignTypeConverterTests.cs new file mode 100644 index 000000000000..15f4ee63aab6 --- /dev/null +++ b/src/Controls/tests/Core.Design.UnitTests/VisibilityDesignTypeConverterTests.cs @@ -0,0 +1,50 @@ +using Microsoft.Maui.Controls.Design; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class VisibilityDesignTypeConverterTests + { + [Fact] + public void VisibilityDesignTypeConverter_StandartValues() + { + VisibilityDesignTypeConverter converter = new VisibilityDesignTypeConverter(); + Assert.True(converter.CanConvertFrom(typeof(string))); + + bool actual = converter.GetStandardValuesSupported(); + Assert.True(actual); + + actual = converter.GetStandardValuesExclusive(); + Assert.True(actual); + + var values = converter.GetStandardValues(); + Assert.Equal(5, values.Count); + } + + [Theory] + [InlineData("true")] + [InlineData(" FALSE ")] + [InlineData("Collapse")] + [InlineData("hidden ")] + [InlineData(" VISIBLE")] + public void VisibilityDesignTypeConverter_Valid(string value) + { + VisibilityDesignTypeConverter converter = new VisibilityDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("Collapse Hidden")] + [InlineData("foo")] + public void VisibilityDesignTypeConverter_Invalid(string value) + { + VisibilityDesignTypeConverter converter = new VisibilityDesignTypeConverter(); + bool actual = converter.IsValid(value); + Assert.False(actual); + } + } +} diff --git a/src/Core/src/Converters/CornerRadiusTypeConverter.cs b/src/Core/src/Converters/CornerRadiusTypeConverter.cs index 4174217d1ca8..31e4fd34b879 100644 --- a/src/Core/src/Converters/CornerRadiusTypeConverter.cs +++ b/src/Core/src/Converters/CornerRadiusTypeConverter.cs @@ -15,6 +15,7 @@ public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destina public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) { + // IMPORTANT! Update CornerRadiusDesignTypeConverter.IsValid if making changes here var strValue = value?.ToString(); if (strValue != null) diff --git a/src/Core/src/Converters/ThicknessTypeConverter.cs b/src/Core/src/Converters/ThicknessTypeConverter.cs index 881dd72f7750..1910c2cc0862 100644 --- a/src/Core/src/Converters/ThicknessTypeConverter.cs +++ b/src/Core/src/Converters/ThicknessTypeConverter.cs @@ -17,6 +17,7 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { + // IMPORTANT! Update ThicknessTypeDesignConverter.IsValid if making changes here var strValue = value?.ToString(); if (strValue != null) diff --git a/src/Graphics/src/Graphics/Point.cs b/src/Graphics/src/Graphics/Point.cs index 1ae71e280d88..895d5d8cc618 100644 --- a/src/Graphics/src/Graphics/Point.cs +++ b/src/Graphics/src/Graphics/Point.cs @@ -129,6 +129,7 @@ public void Deconstruct(out double x, out double y) public static bool TryParse(string value, out Point point) { + // IMPORTANT! Update RectTypeDesignConverter.IsValid if making changes here if (!string.IsNullOrEmpty(value)) { string[] xy = value.Split(','); diff --git a/src/Graphics/src/Graphics/Rect.cs b/src/Graphics/src/Graphics/Rect.cs index 9fe25894d6c0..78a6cf1d41a0 100644 --- a/src/Graphics/src/Graphics/Rect.cs +++ b/src/Graphics/src/Graphics/Rect.cs @@ -221,6 +221,7 @@ public void Deconstruct(out double x, out double y, out double width, out double public static bool TryParse(string value, out Rect rectangle) { + // IMPORTANT! Update RectTypeDesignConverter.IsValid if making changes here if (!string.IsNullOrEmpty(value)) { string[] xywh = value.Split(',');