diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs
index f16639625..8b395e858 100644
--- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs
+++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs
@@ -802,6 +802,14 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility)
{
string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo);
+ // These helpers reject null values, and empty 'string' values also marshal to a null 'HSTRING'
+ string? xamlBindingHelperFallbackCondition = propertyInfo.XamlBindingHelperSetMethodName switch
+ {
+ "SetPropertyFromString" => "value is null || value.Length == 0",
+ "SetPropertyFromUri" => "value is null",
+ _ => null
+ };
+
// Declare the property
writer.WriteLine(skipIfPresent: true);
writer.WriteLine("/// ");
@@ -845,19 +853,18 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility)
field = value;
""", isMultiline: true);
- // If the property is of type 'string', we need a special path. That is because 'XamlBindingHelper.SetPropertyFromString'
- // doesn't work correctly for 'null' or empty strings, so we need to fall back to 'SetValue' in those cases.
- if (propertyInfo.TypeName == "string")
+ // Fall back to 'SetValue' for values rejected by the selected helper
+ if (xamlBindingHelperFallbackCondition is not null)
{
writer.Write($$"""
- if (value is null || value.Length == 0)
+ if ({{xamlBindingHelperFallbackCondition}})
{
SetValue({{propertyInfo.PropertyName}}Property, value);
}
else
{
- global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.SetPropertyFromString(this, {{propertyInfo.PropertyName}}Property, value);
+ global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.{{propertyInfo.XamlBindingHelperSetMethodName}}(this, {{propertyInfo.PropertyName}}Property, value);
}
On{{propertyInfo.PropertyName}}Changed(value);
@@ -942,21 +949,21 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility)
}
""", isMultiline: true);
- // For 'string' properties, we need a specialized path (see comment in the local caching branch above)
- if (propertyInfo.TypeName == "string")
+ // Match the 'string' and 'Uri' fallback from the local caching branch
+ if (xamlBindingHelperFallbackCondition is not null)
{
writer.WriteLine($$"""
{{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set
{
On{{propertyInfo.PropertyName}}Set(ref value);
- if (value is null || value.Length == 0)
+ if ({{xamlBindingHelperFallbackCondition}})
{
SetValue({{propertyInfo.PropertyName}}Property, value);
}
else
{
- global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.SetPropertyFromString(this, {{propertyInfo.PropertyName}}Property, value);
+ global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.{{setMethodName}}(this, {{propertyInfo.PropertyName}}Property, value);
}
On{{propertyInfo.PropertyName}}Changed(value);
diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs
index c4b7250a5..3bfcb46d1 100644
--- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs
+++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs
@@ -4125,6 +4125,12 @@ public partial string? Name
[DataRow("string", "string", "object", "null", "", "SetPropertyFromString")]
[DataRow("string", "string?", "object?", "null", "", "SetPropertyFromString")]
+ // The 'Uri' helper also requires a fallback for 'null' values
+ [DataRow("global::System.Uri", "global::System.Uri", "object", "null", "", "SetPropertyFromUri")]
+ [DataRow("global::System.Uri", "global::System.Uri?", "object?", "null", "", "SetPropertyFromUri")]
+ [DataRow("global::System.Uri", "global::System.Uri", "object", "null", "public partial class MyControl { partial void OnNameSet(ref global::System.Uri propertyValue) { propertyValue = null!; } }", "SetPropertyFromUri")]
+ [DataRow("global::System.Uri", "global::System.Uri?", "object?", "null", "public partial class MyControl { partial void OnNameSet(ref global::System.Uri? propertyValue) { propertyValue = null; } }", "SetPropertyFromUri")]
+
// Well known WinRT primitive types
[DataRow("int", "int", "object", "null", "", "SetPropertyFromInt32")]
[DataRow("byte", "byte", "object", "null", "", "SetPropertyFromByte")]
@@ -4181,9 +4187,7 @@ public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized(
string? typeDefinition = "",
string? setMethodName = null)
{
- // Compute the setter body and partial method block based on whether the optimization is used.
- // The 'string' type needs a special path, since 'XamlBindingHelper.SetPropertyFromString' doesn't
- // handle 'null' or empty strings correctly, so we need to fall back to 'SetValue' in those cases.
+ // The 'string' and 'Uri' helpers need fallbacks for values they cannot accept
string setterBody = setMethodName switch
{
"SetPropertyFromString" => """
@@ -4196,6 +4200,16 @@ public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized(
global::Windows.UI.Xaml.Markup.XamlBindingHelper.SetPropertyFromString(this, NameProperty, value);
}
""",
+ "SetPropertyFromUri" => """
+ if (value is null)
+ {
+ SetValue(NameProperty, value);
+ }
+ else
+ {
+ global::Windows.UI.Xaml.Markup.XamlBindingHelper.SetPropertyFromUri(this, NameProperty, value);
+ }
+ """,
not null => $"""
global::Windows.UI.Xaml.Markup.XamlBindingHelper.{setMethodName}(this, NameProperty, value);
""",
@@ -6023,26 +6037,36 @@ public partial T2? Number
}
[TestMethod]
- public void SingleProperty_Int32_WithNoCaching_WithObjectSetCallback()
+ [DataRow("int", "int", "object")]
+ [DataRow("string", "string", "object")]
+ [DataRow("string", "string?", "object?")]
+ [DataRow("global::System.Uri", "global::System.Uri", "object")]
+ [DataRow("global::System.Uri", "global::System.Uri?", "object?")]
+ public void SingleProperty_MultipleTypes_WithNoCaching_WithObjectSetCallback(
+ string dependencyPropertyType,
+ string propertyType,
+ string boxedPropertyType)
{
- const string source = """
+ string source = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
+ #nullable enable
+
namespace MyNamespace;
public partial class MyControl : DependencyObject
{
[GeneratedDependencyProperty]
- public partial int Number { get; set; }
+ public partial {{propertyType}} Number { get; set; }
- partial void OnNumberSet(ref object propertyValue)
+ partial void OnNumberSet(ref {{boxedPropertyType}} propertyValue)
{
}
}
""";
- const string result = """
+ string result = $$"""
//
#pragma warning disable
#nullable enable
@@ -6058,7 +6082,7 @@ partial class MyControl
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register(
name: "Number",
- propertyType: typeof(int),
+ propertyType: typeof({{dependencyPropertyType}}),
ownerType: typeof(MyControl),
typeMetadata: null);
@@ -6066,7 +6090,7 @@ partial class MyControl
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- public partial int Number
+ public partial {{propertyType}} Number
{
get
{
@@ -6074,7 +6098,7 @@ public partial int Number
OnNumberGet(ref __boxedValue);
- int __unboxedValue = (int)__boxedValue;
+ {{propertyType}} __unboxedValue = ({{propertyType}})__boxedValue;
OnNumberGet(ref __unboxedValue);
@@ -6098,31 +6122,31 @@ public partial int Number
/// The raw property value that has been retrieved from .
/// This method is invoked on the boxed value retrieved via on .
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
- partial void OnNumberGet(ref object propertyValue);
+ partial void OnNumberGet(ref {{boxedPropertyType}} propertyValue);
/// Executes the logic for when the accessor is invoked
/// The unboxed property value that has been retrieved from .
/// This method is invoked on the unboxed value retrieved via on .
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
- partial void OnNumberGet(ref int propertyValue);
+ partial void OnNumberGet(ref {{propertyType}} propertyValue);
/// Executes the logic for when the accessor is invoked
/// The boxed property value that has been produced before assigning to .
/// This method is invoked on the boxed value that is about to be passed to on .
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
- partial void OnNumberSet(ref object propertyValue);
+ partial void OnNumberSet(ref {{boxedPropertyType}} propertyValue);
/// Executes the logic for when the accessor is invoked
/// The property value that is being assigned to .
/// This method is invoked on the raw value being assigned to , before is used.
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
- partial void OnNumberSet(ref int propertyValue);
+ partial void OnNumberSet(ref {{propertyType}} propertyValue);
/// Executes the logic for when has just changed.
/// The new property value that has been set.
/// This method is invoked right after the value of is changed.
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
- partial void OnNumberChanged(int newValue);
+ partial void OnNumberChanged({{propertyType}} newValue);
/// Executes the logic for when has just changed.
/// Event data that is issued by any event that tracks changes to the effective value of this property.
@@ -6141,4 +6165,290 @@ public partial int Number
CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview);
}
+
+ [TestMethod]
+ [DataRow("global::System.Uri", "object")]
+ [DataRow("global::System.Uri?", "object?")]
+ [DataRow("global::System.Uri", "object", true)]
+ [DataRow("global::System.Uri?", "object?", true)]
+ public void SingleProperty_Uri_WithLocalCache(
+ string propertyType,
+ string boxedPropertyType,
+ bool implementSetCallback = false)
+ {
+ string setCallback = implementSetCallback ? $$"""
+ partial void OnNameSet(ref {{propertyType}} propertyValue)
+ {
+ propertyValue = null!;
+ }
+ """ : "";
+
+ string source = $$"""
+ using CommunityToolkit.WinUI;
+ using Windows.UI.Xaml;
+
+ #nullable enable
+
+ namespace MyNamespace;
+
+ public partial class MyControl : DependencyObject
+ {
+ [GeneratedDependencyProperty(IsLocalCacheEnabled = true)]
+ public partial {{propertyType}} Name { get; set; }
+
+ {{setCallback}}
+ }
+ """;
+
+ string result = $$"""
+ //
+ #pragma warning disable
+ #nullable enable
+
+ namespace MyNamespace
+ {
+ ///
+ partial class MyControl
+ {
+ ///
+ /// The backing instance for .
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register(
+ name: "Name",
+ propertyType: typeof(global::System.Uri),
+ ownerType: typeof(MyControl),
+ typeMetadata: null);
+
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ [global::System.Diagnostics.DebuggerNonUserCode]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial {{propertyType}} Name
+ {
+ get => field;
+ set
+ {
+ OnNameSet(ref value);
+
+ if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value))
+ {
+ return;
+ }
+
+ global::System.Uri? __oldValue = field;
+
+ OnNameChanging(value);
+ OnNameChanging(__oldValue, value);
+
+ field = value;
+
+ if (value is null)
+ {
+ SetValue(NameProperty, value);
+ }
+ else
+ {
+ global::Windows.UI.Xaml.Markup.XamlBindingHelper.SetPropertyFromUri(this, NameProperty, value);
+ }
+
+ OnNameChanged(value);
+ OnNameChanged(__oldValue, value);
+ }
+ }
+
+ /// Executes the logic for when the accessor is invoked
+ /// The boxed property value that has been produced before assigning to .
+ /// This method is invoked on the boxed value that is about to be passed to on .
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameSet(ref {{boxedPropertyType}} propertyValue);
+
+ /// Executes the logic for when the accessor is invoked
+ /// The property value that is being assigned to .
+ /// This method is invoked on the raw value being assigned to , before is used.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameSet(ref {{propertyType}} propertyValue);
+
+ /// Executes the logic for when is changing.
+ /// The new property value being set.
+ /// This method is invoked right before the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanging({{propertyType}} newValue);
+
+ /// Executes the logic for when is changing.
+ /// The previous property value that is being replaced.
+ /// The new property value being set.
+ /// This method is invoked right before the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanging(global::System.Uri? oldValue, {{propertyType}} newValue);
+
+ /// Executes the logic for when has just changed.
+ /// The new property value that has been set.
+ /// This method is invoked right after the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanged({{propertyType}} newValue);
+
+ /// Executes the logic for when has just changed.
+ /// The previous property value that has been replaced.
+ /// The new property value that has been set.
+ /// This method is invoked right after the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanged(global::System.Uri? oldValue, {{propertyType}} newValue);
+
+ /// Executes the logic for when has just changed.
+ /// Event data that is issued by any event that tracks changes to the effective value of this property.
+ /// This method is invoked by the infrastructure, after the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e);
+
+ /// Executes the logic for when any dependency property has just changed.
+ /// Event data that is issued by any event that tracks changes to the effective value of this property.
+ /// This method is invoked by the infrastructure, after the value of any dependency property has just changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e);
+ }
+ }
+ """;
+
+ CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ [DataRow("string", "string", "object")]
+ [DataRow("string", "string?", "object?")]
+ [DataRow("global::System.Uri", "global::System.Uri", "object")]
+ [DataRow("global::System.Uri", "global::System.Uri?", "object?")]
+ public void SingleProperty_ReferenceType_WithLocalCache_WithObjectSetCallback(
+ string dependencyPropertyType,
+ string propertyType,
+ string boxedPropertyType)
+ {
+ string source = $$"""
+ using CommunityToolkit.WinUI;
+ using Windows.UI.Xaml;
+
+ #nullable enable
+
+ namespace MyNamespace;
+
+ public partial class MyControl : DependencyObject
+ {
+ [GeneratedDependencyProperty(IsLocalCacheEnabled = true)]
+ public partial {{propertyType}} Name { get; set; }
+
+ partial void OnNameSet(ref {{boxedPropertyType}} propertyValue)
+ {
+ propertyValue = null!;
+ }
+ }
+ """;
+
+ string result = $$"""
+ //
+ #pragma warning disable
+ #nullable enable
+
+ namespace MyNamespace
+ {
+ ///
+ partial class MyControl
+ {
+ ///
+ /// The backing instance for .
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register(
+ name: "Name",
+ propertyType: typeof({{dependencyPropertyType}}),
+ ownerType: typeof(MyControl),
+ typeMetadata: null);
+
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ [global::System.Diagnostics.DebuggerNonUserCode]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial {{propertyType}} Name
+ {
+ get => field;
+ set
+ {
+ OnNameSet(ref value);
+
+ if (global::System.Collections.Generic.EqualityComparer<{{dependencyPropertyType}}?>.Default.Equals(field, value))
+ {
+ return;
+ }
+
+ {{dependencyPropertyType}}? __oldValue = field;
+
+ OnNameChanging(value);
+ OnNameChanging(__oldValue, value);
+
+ field = value;
+
+ object? __boxedValue = value;
+ OnNameSet(ref __boxedValue);
+
+ SetValue(NameProperty, __boxedValue);
+
+ OnNameChanged(value);
+ OnNameChanged(__oldValue, value);
+ }
+ }
+
+ /// Executes the logic for when the accessor is invoked
+ /// The boxed property value that has been produced before assigning to .
+ /// This method is invoked on the boxed value that is about to be passed to on .
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameSet(ref {{boxedPropertyType}} propertyValue);
+
+ /// Executes the logic for when the accessor is invoked
+ /// The property value that is being assigned to .
+ /// This method is invoked on the raw value being assigned to , before is used.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameSet(ref {{propertyType}} propertyValue);
+
+ /// Executes the logic for when is changing.
+ /// The new property value being set.
+ /// This method is invoked right before the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanging({{propertyType}} newValue);
+
+ /// Executes the logic for when is changing.
+ /// The previous property value that is being replaced.
+ /// The new property value being set.
+ /// This method is invoked right before the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanging({{dependencyPropertyType}}? oldValue, {{propertyType}} newValue);
+
+ /// Executes the logic for when has just changed.
+ /// The new property value that has been set.
+ /// This method is invoked right after the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanged({{propertyType}} newValue);
+
+ /// Executes the logic for when has just changed.
+ /// The previous property value that has been replaced.
+ /// The new property value that has been set.
+ /// This method is invoked right after the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNameChanged({{dependencyPropertyType}}? oldValue, {{propertyType}} newValue);
+
+ /// Executes the logic for when has just changed.
+ /// Event data that is issued by any event that tracks changes to the effective value of this property.
+ /// This method is invoked by the infrastructure, after the value of is changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e);
+
+ /// Executes the logic for when any dependency property has just changed.
+ /// Event data that is issued by any event that tracks changes to the effective value of this property.
+ /// This method is invoked by the infrastructure, after the value of any dependency property has just changed.
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )]
+ partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e);
+ }
+ }
+ """;
+
+ CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview);
+ }
}
diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md
index ddf1c94a1..0c1c694d7 100644
--- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md
+++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md
@@ -76,7 +76,7 @@ Use the properties like any other dependency properties. For example, after mapp
You can also use `CounterControl.CountProperty` with APIs such as `GetValue`, `SetValue`, and `ClearValue`. Do not declare the identifier field or another implementation of the partial property yourself.
-Without local caching, the getter reads the effective value from the XAML property system. The setter writes to that system, using optimized `XamlBindingHelper` APIs where available and appropriate. The generator handles the details, including falling back to `SetValue` for null or empty strings.
+Without local caching, the getter reads the effective value from the XAML property system. The setter writes to that system, using optimized `XamlBindingHelper` APIs where available and appropriate. The generator falls back to `SetValue` for `null` URI values and `null` or empty strings, in both caching modes. These checks use the value after the typed setter hook, even for properties not annotated as nullable.
## Attribute options
@@ -263,7 +263,7 @@ public partial class NormalizedCounter : DependencyObject
}
```
-For ordinary properties without local caching, implementing the boxed setter hook makes the generator use the `SetValue` path instead of a typed `XamlBindingHelper` optimization, so the hook can inspect or replace the boxed value. The boxed getter hook can normalize values before a cast that would otherwise fail. Replacements must remain compatible with the declared property and metadata types.
+Implementing the boxed setter hook makes the generator use the `SetValue` path instead of a typed `XamlBindingHelper` optimization, so the hook can inspect or replace the boxed value. This applies with and without local caching. The boxed getter hook can normalize values before a cast that would otherwise fail. Replacements must remain compatible with the declared property and metadata types.
An object-typed property has only one getter hook and one setter hook, rather than duplicate typed and boxed signatures. Nullable annotations on hook parameters follow the generated property's signatures.