diff --git a/src/Uno.UI.RuntimeTests/Tests/Uno_Helpers/Given_DependencyPropertyHelper.cs b/src/Uno.UI.RuntimeTests/Tests/Uno_Helpers/Given_DependencyPropertyHelper.cs new file mode 100644 index 000000000000..1d134e4fc362 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Uno_Helpers/Given_DependencyPropertyHelper.cs @@ -0,0 +1,238 @@ +#if HAS_UNO // DependencyPropertyHelper is only available on Uno +#nullable enable + +using System; +using FluentAssertions; +using Microsoft.UI.Xaml; +using Uno.UI.Xaml.Core; + +namespace Uno.UI.RuntimeTests.Tests.Uno_Helpers; + +[TestClass] +[RunsOnUIThread] +public partial class Given_DependencyPropertyHelper +{ + [TestMethod] + public void When_GetDefaultValue() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var defaultValue = DependencyPropertyHelper.GetDefaultValue(property); + + // Assert + defaultValue.Should().Be("TestValue"); + } + + [TestMethod] + public void When_GetDefaultValue_OverrideMetadata_GenericT() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var defaultValue = DependencyPropertyHelper.GetDefaultValue(property); + + // Assert + defaultValue.Should().Be("OverridenTestValue"); + } + + [TestMethod] + public void When_GetDefaultValue_OverrideMetadata_Type() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var defaultValue = DependencyPropertyHelper.GetDefaultValue(property, typeof(DerivedTestClass)); + + // Assert + defaultValue.Should().Be("OverridenTestValue"); + } + + [TestMethod] + public void When_GetDependencyPropertyByName_OwnerType() + { + // Arrange + var propertyName = "TestProperty"; + + // Act + var property1 = DependencyPropertyHelper.GetDependencyPropertyByName(typeof(TestClass), propertyName); + var property2 = DependencyPropertyHelper.GetDependencyPropertyByName(typeof(DerivedTestClass), propertyName); + + // Assert + property1.Should().Be(TestClass.TestProperty); + property2.Should().Be(TestClass.TestProperty); + } + + [TestMethod] + public void When_GetDependencyPropertyByName_Property() + { + // Arrange + var propertyName = "TestProperty"; + + // Act + var property1 = DependencyPropertyHelper.GetDependencyPropertyByName(propertyName); + var property2 = DependencyPropertyHelper.GetDependencyPropertyByName(propertyName); + + // Assert + property1.Should().Be(TestClass.TestProperty); + property2.Should().Be(TestClass.TestProperty); + } + + [TestMethod] + public void When_GetDependencyPropertyByName_InvalidProperty() + { + // Arrange + var propertyName = "InvalidProperty"; + + // Act + var property = DependencyPropertyHelper.GetDependencyPropertyByName(typeof(TestClass), propertyName); + + // Assert + property.Should().BeNull(); + } + + [TestMethod] + public void When_GetDependencyPropertyByName_InvalidPropertyCasing() + { + // Arrange + var propertyName = "testProperty"; + + // Act + var property = DependencyPropertyHelper.GetDependencyPropertyByName(typeof(TestClass), propertyName); + + // Assert + property.Should().BeNull(); + } + + [TestMethod] + public void When_GetDependencyPropertiesForType() + { + // Arrange + var properties = DependencyPropertyHelper.GetDependencyPropertiesForType(); + + // Assert + properties.Should().Contain(TestClass.TestProperty); + } + + [TestMethod] + public void When_TryGetDependencyPropertiesForType() + { + // Arrange + var success = DependencyPropertyHelper.TryGetDependencyPropertiesForType(typeof(TestClass), out var properties); + + // Assert + success.Should().BeTrue(); + properties.Should().Contain(TestClass.TestProperty); + } + + [TestMethod] + public void When_TryGetDependencyPropertiesForType_Invalid() + { + // Arrange + var success = DependencyPropertyHelper.TryGetDependencyPropertiesForType(typeof(string), out var properties); + + // Assert + success.Should().BeFalse(); + properties.Should().BeNull(); + } + + [TestMethod] + public void When_GetPropertyType() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var propertyType = DependencyPropertyHelper.GetPropertyType(property); + + // Assert + propertyType.Should().Be(typeof(string)); + } + + [TestMethod] + public void When_GetPropertyType_OverrideMetadata_GenericT() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var propertyType = DependencyPropertyHelper.GetPropertyType(property); + + // Assert + propertyType.Should().Be(typeof(string)); + } + + [TestMethod] + public void When_GetPropertyDetails() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var (valueType, ownerType, name, isTypeNullable, isAttached, inInherited, defaultValue) = DependencyPropertyHelper.GetPropertyDetails(property); + + // Assert + valueType.Should().Be(typeof(string)); + ownerType.Should().Be(typeof(TestClass)); + name.Should().Be("TestProperty"); + isTypeNullable.Should().BeFalse(); + isAttached.Should().BeFalse(); + inInherited.Should().BeFalse(); + defaultValue.Should().Be("TestValue"); + } + + [TestMethod] + public void When_GetPropertyDetails_ForType() + { + // Arrange + var property = TestClass.TestProperty; + + // Act + var (valueType, ownerType, name, isTypeNullable, isAttached, inInherited, defaultValue) = DependencyPropertyHelper.GetPropertyDetails(property, typeof(DerivedTestClass)); + + // Assert + valueType.Should().Be(typeof(string)); + ownerType.Should().Be(typeof(TestClass)); + name.Should().Be("TestProperty"); + isTypeNullable.Should().BeFalse(); + isAttached.Should().BeFalse(); + inInherited.Should().BeFalse(); + defaultValue.Should().Be("OverridenTestValue"); + } + + [TestMethod] + public void When_GetPropertyDetails_DataContext() + { + // Arrange + var property = FrameworkElement.DataContextProperty; + + // Act + var (valueType, ownerType, name, isTypeNullable, isAttached, inInherited, defaultValue) = DependencyPropertyHelper.GetPropertyDetails(property); + + // Assert + valueType.Should().Be(typeof(object)); + ownerType.Should().Be(typeof(FrameworkElement)); + name.Should().Be("DataContext"); + isTypeNullable.Should().BeTrue(); + isAttached.Should().BeFalse(); + inInherited.Should().BeTrue(); + defaultValue.Should().BeNull(); + } + + private partial class TestClass : FrameworkElement // Not a DependencyObject because we don't want to deal with the generator here + { + public static readonly DependencyProperty TestProperty = DependencyProperty.Register("TestProperty", typeof(string), typeof(TestClass), new PropertyMetadata("TestValue")); + } + + private partial class DerivedTestClass : TestClass + { + public DerivedTestClass() + { + TestProperty.OverrideMetadata(GetType(), new PropertyMetadata("OverridenTestValue")); + } + } +} +#endif diff --git a/src/Uno.UI/UI/Xaml/Internal/DependencyPropertyHelper.cs b/src/Uno.UI/UI/Xaml/Internal/DependencyPropertyHelper.cs new file mode 100644 index 000000000000..bf35f0f8b18e --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/DependencyPropertyHelper.cs @@ -0,0 +1,216 @@ +#nullable enable + +using Microsoft.UI.Xaml; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Uno.UI.Xaml.Core; + +/// +/// The goal of this class is to provide a set of helper methods to work with from +/// external projects. +/// +/// +/// A small reflection is still required to access the class because those using it needs to know what they are doing. +/// +internal static class DependencyPropertyHelper +{ + /// + /// Get the default value of a for a given type. + /// + /// + /// This is the value defined in the metadata of the property, which _may_ be overriden + /// using the internal DependencyProperty.OverrideMetadata() method on a per-type basis + /// -- that's why the type parameter is required. + /// + /// A classic example of that is the Button.VerticalAlignmentProperty which is overridden + /// to be VerticalAlignment.Center, while the default value is VerticalAlignment.Stretch + /// from the base class. + /// + public static object? GetDefaultValue(DependencyProperty dependencyProperty, Type forType) + => dependencyProperty.GetMetadata(forType)?.DefaultValue; + + /// + /// Get the default value of a for a given type. + /// + /// + /// This is the value defined in the metadata of the property, which _may_ be overriden + /// using the internal DependencyProperty.OverrideMetadata() method on a per-type basis + /// -- that's why the type parameter is required. + /// + /// A classic example of that is the Button.VerticalAlignmentProperty which is overridden + /// to be VerticalAlignment.Center, while the default value is VerticalAlignment.Stretch + /// from the base class. + /// + public static object? GetDefaultValue(DependencyProperty dependencyProperty) + where T : DependencyObject + => dependencyProperty.GetMetadata(typeof(T))?.DefaultValue; + + /// + /// Get the default value of a for the owner type. + /// + /// + /// This is the value defined in the metadata of the property, which _may_ be overriden + /// using the internal DependencyProperty.OverrideMetadata() method on a per-type basis + /// -- that's why the type parameter is required. + /// + /// A classic example of that is the Button.VerticalAlignmentProperty which is overridden + /// to be VerticalAlignment.Center, while the default value is VerticalAlignment.Stretch + /// from the base class. + /// + /// This overload will return the default value for the owner type of the property - where + /// the value is originally defined. + /// + public static object? GetDefaultValue(DependencyProperty dependencyProperty) + => dependencyProperty.GetMetadata(dependencyProperty.OwnerType)?.DefaultValue; + + /// + /// Get a reference to by its name and owner type. + /// + /// + /// The name will match the name of the property as defined when the property is registered. + /// + /// There is usually NO "Property" suffix on that name since it's the name that is used in XAML. + /// + /// The name is case-sensitive. + /// + public static DependencyProperty? GetDependencyPropertyByName(Type ownerType, string propertyName) + => DependencyProperty.GetProperty(ownerType, propertyName); + + /// + /// Get a reference to by its name on the given type. + /// + /// + /// The name will match the name of the property as defined when the property is registered. + /// + /// There is usually NO "Property" suffix on that name since it's the name that is used in XAML. + /// + /// The name is case-sensitive. + /// + public static DependencyProperty? GetDependencyPropertyByName(string propertyName) + where T : DependencyObject + => GetDependencyPropertyByName(typeof(T), propertyName); + + /// + /// Get all the defined for a given type. + /// + public static IReadOnlyCollection? GetDependencyPropertiesForType() + where T : DependencyObject + => DependencyProperty.GetPropertiesForType(typeof(T)); + + /// + /// Try to get all the defined for a given type. + /// + /// False means it's not a dependency object + public static bool TryGetDependencyPropertiesForType( + Type forType, + [NotNullWhen(true)] out IReadOnlyCollection? properties) + { + // Check if type is a DependencyObject + if (!typeof(DependencyObject).IsAssignableFrom(forType)) + { + properties = null; + return false; + } + + properties = DependencyProperty.GetPropertiesForType(forType); + return true; + } + + /// + /// Get the value type of the property. + /// + public static Type GetPropertyType(DependencyProperty dependencyProperty) + => dependencyProperty.Type; + + /// + /// Get the name of the property. + /// + public static string GetPropertyName(DependencyProperty dependencyProperty) + => dependencyProperty.Name; + + /// + /// Get the owner type of the property + /// + /// + /// This is the property that defines the property, not the type that uses it. + /// It may also be overridden by a derived type. + /// + public static Type GetPropertyOwnerType(DependencyProperty dependencyProperty) + => dependencyProperty.OwnerType; + + /// + /// Get whether the property is an Attached Property. + /// + public static bool GetPropertyIsAttached(DependencyProperty dependencyProperty) + => dependencyProperty.IsAttached; + + /// + /// Get whether the property type is nullable - if the _null_ value is a valid value for the property. + /// + public static bool GetPropertyIsTypeNullable(DependencyProperty dependencyProperty) + => dependencyProperty.IsTypeNullable; + + /// + /// Get the table of values for a given property on a given object detailing the value for each precedence. + /// + public static (object value, DependencyPropertyValuePrecedences precedence)[] GetValueForEachPrecedences( + DependencyProperty dependencyProperty, + DependencyObject obj) + => obj.GetValueForEachPrecedences(dependencyProperty); + + /// + /// Set the value of a property on an object for a given precedence. + /// + /// + /// You must know what you are doing when using this method as it can break the property system. + /// + public static void SetValueForPrecedence( + DependencyObject obj, + DependencyProperty dependencyProperty, + object value, + DependencyPropertyValuePrecedences precedence) + => obj.SetValue(dependencyProperty, value, precedence); + + /// + /// Get if the property value is inherited through the visual tree. + /// + public static bool GetPropertyIsInherited(DependencyProperty dependencyProperty) + => dependencyProperty.GetMetadata(dependencyProperty.OwnerType) is FrameworkPropertyMetadata metadata + && metadata.Options.HasFlag(FrameworkPropertyMetadataOptions.Inherits); + + /// + /// Get if the property value is inherited through the visual tree. + /// + public static bool GetPropertyIsInherited(DependencyProperty dependencyProperty, Type forType) + => dependencyProperty.GetMetadata(forType) is FrameworkPropertyMetadata metadata + && metadata.Options.HasFlag(FrameworkPropertyMetadataOptions.Inherits); + + /// + /// Get the multiple aspects of a given property at the same time. + /// + public static (Type ValueType, Type OwnerType, string Name, bool IsTypeNullable, bool IsAttached, bool IsInherited, object? defaultValue) GetPropertyDetails( + DependencyProperty property) + => (property.Type, + property.OwnerType, + property.Name, + property.IsTypeNullable, + property.IsAttached, + property.GetMetadata(property.OwnerType) is FrameworkPropertyMetadata metadata && metadata.Options.HasFlag(FrameworkPropertyMetadataOptions.Inherits), + property.GetMetadata(property.OwnerType)?.DefaultValue); + + /// + /// Get the multiple aspects of a given property at the same time. + /// + public static (Type ValueType, Type OwnerType, string Name, bool IsTypeNullable, bool IsAttached, bool IsInherited, object? defaultValue) GetPropertyDetails( + DependencyProperty property, + Type forType) + => (property.Type, + property.OwnerType, + property.Name, + property.IsTypeNullable, + property.IsAttached, + property.GetMetadata(forType) is FrameworkPropertyMetadata metadata && metadata.Options.HasFlag(FrameworkPropertyMetadataOptions.Inherits), + property.GetMetadata(forType)?.DefaultValue); +}