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..5da739a50841 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Uno_Helpers/Given_DependencyPropertyHelper.cs @@ -0,0 +1,259 @@ +#if HAS_UNO // DependencyPropertyHelper is only available on Uno +#nullable enable + +using System; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +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_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.GetValueType(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.GetDetails(property); + + // Assert + using var _ = new AssertionScope(); + valueType.Should().Be(typeof(string)); + ownerType.Should().Be(typeof(TestClass)); + name.Should().Be("TestProperty"); + isTypeNullable.Should().BeTrue(); + isAttached.Should().BeFalse(); + inInherited.Should().BeFalse(); + defaultValue.Should().Be("TestValue"); + } + + [TestMethod] + public void When_GetPropertyDetails_DataContext() + { + // Arrange + var property = UIElement.DataContextProperty; + + // Act + var (valueType, _, name, isTypeNullable, isAttached, inInherited, defaultValue) = DependencyPropertyHelper.GetDetails(property); + + // Assert + using var _ = new AssertionScope(); + valueType.Should().Be(typeof(object)); + // ownerType is not checked here because it's different following the platform + name.Should().Be("DataContext"); + isTypeNullable.Should().BeTrue(); + isAttached.Should().BeFalse(); + inInherited.Should().BeTrue(); + defaultValue.Should().BeNull(); + } + + [TestMethod] + public void When_GetPropertyDetails_Attached() + { + // Arrange + var property = Grid.RowProperty; + + // Act + var (valueType, ownerType, name, isTypeNullable, isAttached, inInherited, defaultValue) = DependencyPropertyHelper.GetDetails(property); + + // Assert + using var _ = new AssertionScope(); + valueType.Should().Be(typeof(int)); + ownerType.Should().Be(typeof(Grid)); + name.Should().Be("Row"); + isTypeNullable.Should().BeFalse(); + isAttached.Should().BeTrue(); + inInherited.Should().BeFalse(); + defaultValue.Should().Be(0); + } + + [TestMethod] + public void When_GetProperties() + { + var properties = DependencyPropertyHelper.GetDependencyPropertiesForType(); + + properties.Should().Contain(DerivedTestClass.TestProperty); + } + + [TestMethod] + public void When_GetDefaultValue_Derived() + { + // Arrange + var property = DerivedTestClass.TestProperty; + + // Act + var defaultValue = DependencyPropertyHelper.GetDefaultValue(property); + + // Assert + defaultValue.Should().Be("TestValue"); + } + + [TestMethod] + public void When_GetDefaultUnsetValue_FromStyle() + { + // Arrange + var sut = new DerivedTestClass(); + sut.SetValue(TestClass.TestProperty, "Something"); + + // Act + var (unsetValue, precedence) = DependencyPropertyHelper.GetDefaultUnsetValue(sut, TestClass.TestProperty); + + // Assert + unsetValue.Should().Be("StyledTestValue"); + precedence.Should().Be(DependencyPropertyValuePrecedences.ExplicitStyle); + } + + [TestMethod] + public void When_GetDefaultUnsetValue() + { + // Arrange + var sut = new TestClass(); + sut.SetValue(TestClass.TestProperty, "Something"); + + // Act + var (unsetValue, precedence) = DependencyPropertyHelper.GetDefaultUnsetValue(sut, TestClass.TestProperty); + + // Assert + unsetValue.Should().Be("TestValue"); + precedence.Should().Be(DependencyPropertyValuePrecedences.DefaultValue); + } + + + 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() + { + Style = new Style(typeof(DerivedTestClass)) + { + Setters = { new Setter(TestProperty, "StyledTestValue") } + }; + } + } +} +#endif diff --git a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs index 5e1a36adaafc..19b06f97a100 100644 --- a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs +++ b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs @@ -447,9 +447,7 @@ public void SetValue(DependencyProperty property, object value) /// /// Clears the value for the specified dependency property on the specified instance. /// - /// The instance on which the property is attached /// The dependency property to get - /// The value precedence to assign public void ClearValue(DependencyProperty property) { SetValue(property, DependencyProperty.UnsetValue, DependencyPropertyValuePrecedences.Local); 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..1df1608380ea --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/DependencyPropertyHelper.cs @@ -0,0 +1,209 @@ +#nullable enable + +using Microsoft.UI.Xaml; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.UI.Xaml.Controls; + +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 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 GetValueType(DependencyProperty dependencyProperty) + => dependencyProperty.Type; + + /// + /// Get the name of the property. + /// + public static string GetName(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 GetOwnerType(DependencyProperty dependencyProperty) + => dependencyProperty.OwnerType; + + /// + /// Get whether the property is an Attached Property. + /// + public static bool GetIsAttached(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; + + /// + /// This method is used to get the default value of a property on a dependency object and give the precedence of that value. + /// + /// + /// This method won't check the local value of the property. It's basically what the property would be if it there was no local value. + /// + public static (object? value, DependencyPropertyValuePrecedences precedence) GetDefaultUnsetValue( + DependencyObject obj, + DependencyProperty dependencyProperty) + { + // 1st: Check assigned style value + if (obj is FrameworkElement fe && fe.TryGetValueFromStyle(dependencyProperty, out var valueFromStyle)) + { + // .TryGetValueFromStyle() will return false if the value is UnsetValue + return (valueFromStyle, DependencyPropertyValuePrecedences.ExplicitStyle); + } + + // 2nd: Check built-in style value, if any + if (obj is Control control && control.TryGetValueFromBuiltInStyle(dependencyProperty, out var valueFromImplicitStyle)) + { + // .TryGetValueFromBuiltInStyle() will return false if the value is UnsetValue + // NOTE: ExplicitStyle here actually means ExplicitOrImplicitStyle. This will be fixed with https://github.com/unoplatform/uno/pull/15684/ + return (valueFromImplicitStyle, DependencyPropertyValuePrecedences.ImplicitStyle); + } + + if (obj is IDependencyObjectStoreProvider { Store: { } store } && store.GetPropertyDetails(dependencyProperty) is { } details) + { + // 3rd: Check inherited value + var inheritedValue = details.GetInheritedValue(); + if (inheritedValue != DependencyProperty.UnsetValue) + { + return (details.GetInheritedValue(), DependencyPropertyValuePrecedences.Inheritance); + } + + // 4th: Check default value + var defaultValue = store.GetDefaultValue(dependencyProperty); + if (defaultValue != DependencyProperty.UnsetValue) + { + return (defaultValue, DependencyPropertyValuePrecedences.DefaultValue); + } + } + + // 5th: Return default value of the type (should not happen) + var propertyType = dependencyProperty.Type; + return (propertyType.IsValueType ? Activator.CreateInstance(propertyType) : null, DependencyPropertyValuePrecedences.DefaultValue); + } + + /// + /// 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 GetIsInherited(DependencyProperty dependencyProperty) + => dependencyProperty.GetMetadata(dependencyProperty.OwnerType) 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) GetDetails( + DependencyProperty property) + { + var propertyMetadata = property.GetMetadata(property.OwnerType); + + return (property.Type, + property.OwnerType, + property.Name, + property.IsTypeNullable, + property.IsAttached, + propertyMetadata is FrameworkPropertyMetadata metadata && + metadata.Options.HasFlag(FrameworkPropertyMetadataOptions.Inherits), + propertyMetadata?.DefaultValue); + } +}