Merge pull request #16315 from unoplatform/dev/cdb/dependency-property-helper

feat: New DependencyPropertyHelper class
This commit is contained in:
Carl de Billy 2024-09-13 03:03:05 -04:00 коммит произвёл GitHub
Родитель a3d6e5116e 25c3646aec
Коммит da4292921d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 468 добавлений и 2 удалений

Просмотреть файл

@ -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<TestClass>(propertyName);
var property2 = DependencyPropertyHelper.GetDependencyPropertyByName<TestClass>(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<TestClass>();
// 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<DerivedTestClass>();
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

Просмотреть файл

@ -447,9 +447,7 @@ namespace Microsoft.UI.Xaml
/// <summary>
/// Clears the value for the specified dependency property on the specified instance.
/// </summary>
/// <param name="instance">The instance on which the property is attached</param>
/// <param name="property">The dependency property to get</param>
/// <param name="precedence">The value precedence to assign</param>
public void ClearValue(DependencyProperty property)
{
SetValue(property, DependencyProperty.UnsetValue, DependencyPropertyValuePrecedences.Local);

Просмотреть файл

@ -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;
/// <summary>
/// The goal of this class is to provide a set of helper methods to work with <see cref="DependencyProperty"/> from
/// external projects.
/// </summary>
/// <remarks>
/// A small reflection is still required to access the class because those using it needs to know what they are doing.
/// </remarks>
internal static class DependencyPropertyHelper
{
/// <summary>
/// Get the default value of a <see cref="DependencyProperty"/> for the owner type.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static object? GetDefaultValue(DependencyProperty dependencyProperty)
=> dependencyProperty.GetMetadata(dependencyProperty.OwnerType)?.DefaultValue;
/// <summary>
/// Get a reference to <see cref="DependencyProperty"/> by its name and owner type.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static DependencyProperty? GetDependencyPropertyByName(Type ownerType, string propertyName)
=> DependencyProperty.GetProperty(ownerType, propertyName);
/// <summary>
/// Get a reference to <see cref="DependencyProperty"/> by its name on the given type.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static DependencyProperty? GetDependencyPropertyByName<T>(string propertyName)
where T : DependencyObject
=> GetDependencyPropertyByName(typeof(T), propertyName);
/// <summary>
/// Get all the <see cref="DependencyProperty"/> defined for a given type.
/// </summary>
public static IReadOnlyCollection<DependencyProperty>? GetDependencyPropertiesForType<T>()
where T : DependencyObject
=> DependencyProperty.GetPropertiesForType(typeof(T));
/// <summary>
/// Try to get all the <see cref="DependencyProperty"/> defined for a given type.
/// </summary>
/// <returns>False means it's not a dependency object</returns>
public static bool TryGetDependencyPropertiesForType(
Type forType,
[NotNullWhen(true)] out IReadOnlyCollection<DependencyProperty>? properties)
{
// Check if type is a DependencyObject
if (!typeof(DependencyObject).IsAssignableFrom(forType))
{
properties = null;
return false;
}
properties = DependencyProperty.GetPropertiesForType(forType);
return true;
}
/// <summary>
/// Get the value type of the property.
/// </summary>
public static Type GetValueType(DependencyProperty dependencyProperty)
=> dependencyProperty.Type;
/// <summary>
/// Get the name of the property.
/// </summary>
public static string GetName(DependencyProperty dependencyProperty)
=> dependencyProperty.Name;
/// <summary>
/// Get the owner type of the property
/// </summary>
/// <remarks>
/// This is the property that defines the property, not the type that uses it.
/// It may also be overridden by a derived type.
/// </remarks>
public static Type GetOwnerType(DependencyProperty dependencyProperty)
=> dependencyProperty.OwnerType;
/// <summary>
/// Get whether the property is an Attached Property.
/// </summary>
public static bool GetIsAttached(DependencyProperty dependencyProperty)
=> dependencyProperty.IsAttached;
/// <summary>
/// Get whether the property type is nullable - if the _null_ value is a valid value for the property.
/// </summary>
public static bool GetPropertyIsTypeNullable(DependencyProperty dependencyProperty)
=> dependencyProperty.IsTypeNullable;
/// <summary>
/// This method is used to get the default value of a property on a dependency object and give the precedence of that value.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <summary>
/// Set the value of a property on an object for a given precedence.
/// </summary>
/// <remarks>
/// You must know what you are doing when using this method as it can break the property system.
/// </remarks>
public static void SetValueForPrecedence(
DependencyObject obj,
DependencyProperty dependencyProperty,
object value,
DependencyPropertyValuePrecedences precedence)
=> obj.SetValue(dependencyProperty, value, precedence);
/// <summary>
/// Get if the property value is inherited through the visual tree.
/// </summary>
public static bool GetIsInherited(DependencyProperty dependencyProperty)
=> dependencyProperty.GetMetadata(dependencyProperty.OwnerType) is FrameworkPropertyMetadata metadata
&& metadata.Options.HasFlag(FrameworkPropertyMetadataOptions.Inherits);
/// <summary>
/// Get the multiple aspects of a given property at the same time.
/// </summary>
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);
}
}