Browse Source

Made AvaloniaPropertyMetadata immutable after property initialization (#15384)

* Made AvaloniaPropertyMetadata immutable after property initialization

* Removed redundant throw
Julien Lebosquain 1 year ago
parent
commit
1d18586efc

+ 9 - 4
src/Avalonia.Base/AvaloniaProperty.cs

@@ -50,7 +50,10 @@ namespace Avalonia
             AvaloniaPropertyMetadata metadata,
             Action<AvaloniaObject, bool>? notifying = null)
         {
-            _ = name ?? throw new ArgumentNullException(nameof(name));
+            ThrowHelper.ThrowIfNull(name, nameof(name));
+            ThrowHelper.ThrowIfNull(valueType, nameof(valueType));
+            ThrowHelper.ThrowIfNull(ownerType, nameof(ownerType));
+            ThrowHelper.ThrowIfNull(metadata, nameof(metadata));
 
             if (name.Contains('.'))
             {
@@ -60,12 +63,13 @@ namespace Avalonia
             _metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();
 
             Name = name;
-            PropertyType = valueType ?? throw new ArgumentNullException(nameof(valueType));
-            OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType));
+            PropertyType = valueType;
+            OwnerType = ownerType;
             Notifying = notifying;
             Id = s_nextId++;
 
-            _metadata.Add(hostType, metadata ?? throw new ArgumentNullException(nameof(metadata)));
+            metadata.Freeze();
+            _metadata.Add(hostType, metadata);
             _defaultMetadata = metadata.GenerateTypeSafeMetadata();
             _singleMetadata = new(hostType, metadata);
         }
@@ -584,6 +588,7 @@ namespace Avalonia
 
             var baseMetadata = GetMetadata(type);
             metadata.Merge(baseMetadata, this);
+            metadata.Freeze();
             _metadata.Add(type, metadata);
             _metadataCache.Clear();
 

+ 14 - 0
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@@ -1,3 +1,4 @@
+using System;
 using Avalonia.Data;
 
 namespace Avalonia
@@ -7,6 +8,7 @@ namespace Avalonia
     /// </summary>
     public abstract class AvaloniaPropertyMetadata
     {
+        private bool _isReadOnly;
         private BindingMode _defaultBindingMode;
 
         /// <summary>
@@ -54,6 +56,11 @@ namespace Avalonia
             AvaloniaPropertyMetadata baseMetadata, 
             AvaloniaProperty property)
         {
+            if (_isReadOnly)
+            {
+                throw new InvalidOperationException("The metadata is read-only.");
+            }
+
             if (_defaultBindingMode == BindingMode.Default)
             {
                 _defaultBindingMode = baseMetadata.DefaultBindingMode;
@@ -62,6 +69,13 @@ namespace Avalonia
             EnableDataValidation ??= baseMetadata.EnableDataValidation;
         }
 
+        /// <summary>
+        /// Makes this instance read-only.
+        /// No further modifications are allowed after this call.
+        /// </summary>
+        public void Freeze()
+            => _isReadOnly = true;
+
         /// <summary>
         /// Gets a copy of this object configured for use with any owner type.
         /// </summary>

+ 1 - 0
src/Avalonia.Base/DirectProperty.cs

@@ -94,6 +94,7 @@ namespace Avalonia
                 enableDataValidation: enableDataValidation);
 
             metadata.Merge(GetMetadata<TOwner>(), this);
+            metadata.Freeze();
 
             var result = new DirectProperty<TNewOwner, TValue>(
                 (DirectPropertyBase<TValue>)this,

+ 7 - 1
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@@ -46,6 +46,12 @@ namespace Avalonia
             }
         }
 
-        public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation);
+        /// <inheritdoc />
+        public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata()
+        {
+            var copy = new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation);
+            copy.Freeze();
+            return copy;
+        }
     }
 }

+ 7 - 1
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -59,6 +59,12 @@ namespace Avalonia
             }
         }
 
-        public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new StyledPropertyMetadata<TValue>(DefaultValue, DefaultBindingMode, enableDataValidation: EnableDataValidation ?? false);
+        /// <inheritdoc />
+        public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata()
+        {
+            var copy = new StyledPropertyMetadata<TValue>(DefaultValue, DefaultBindingMode, null, EnableDataValidation ?? false);
+            copy.Freeze();
+            return copy;
+        }
     }
 }

+ 21 - 0
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@@ -82,6 +82,27 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode);
         }
 
+        [Fact]
+        public void Default_Metadata_Cannot_Be_Changed_After_Property_Initialization()
+        {
+            var metadata = new TestMetadata();
+            var property = new TestProperty<string>("test", typeof(Class1), metadata);
+
+            Assert.Throws<InvalidOperationException>(() => metadata.Merge(new TestMetadata(), property));
+        }
+
+        [Fact]
+        public void Overridden_Metadata_Cannot_Be_Changed_After_OverrideMetadata()
+        {
+            var metadata = new TestMetadata(BindingMode.TwoWay);
+            var overridden = new TestMetadata();
+            var property = new TestProperty<string>("test", typeof(Class1), metadata);
+
+            property.OverrideMetadata<Class2>(overridden);
+
+            Assert.Throws<InvalidOperationException>(() => overridden.Merge(new TestMetadata(), property));
+        }
+
         [Fact]
         public void Changed_Observable_Fired()
         {

+ 20 - 0
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@@ -1,3 +1,4 @@
+using System;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
@@ -46,6 +47,25 @@ namespace Avalonia.Base.UnitTests
             Assert.Same(p1.Changed, p2.Changed);
         }
 
+        [Fact]
+        public void Default_GetMetadata_Cannot_Be_Changed()
+        {
+            var p1 = Class1.FooProperty;
+            var metadata = p1.GetMetadata<Class1>();
+
+            Assert.Throws<InvalidOperationException>(() => metadata.Merge(new DirectPropertyMetadata<string>(), p1));
+        }
+
+        [Fact]
+        public void AddOwnered_GetMetadata_Cannot_Be_Changed()
+        {
+            var p1 = Class1.FooProperty;
+            var p2 = p1.AddOwner<Class2>(_ => null, (_, _) => { });
+            var metadata = p2.GetMetadata<Class2>();
+
+            Assert.Throws<InvalidOperationException>(() => metadata.Merge(new DirectPropertyMetadata<string>(), p2));
+        }
+
         private class Class1 : AvaloniaObject
         {
             public static readonly DirectProperty<Class1, string> FooProperty =

+ 28 - 0
tests/Avalonia.Base.UnitTests/StyledPropertyTests.cs

@@ -1,3 +1,4 @@
+using System;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
@@ -32,6 +33,33 @@ namespace Avalonia.Base.UnitTests
             Assert.Same(p1, p2);
         }
 
+        [Fact]
+        public void Default_GetMetadata_Cannot_Be_Changed()
+        {
+            var p1 = new StyledProperty<string>(
+                "p1",
+                typeof(Class1),
+                typeof(Class1),
+                new StyledPropertyMetadata<string>());
+            var metadata = p1.GetMetadata<Class1>();
+
+            Assert.Throws<InvalidOperationException>(() => metadata.Merge(new StyledPropertyMetadata<string>(), p1));
+        }
+
+        [Fact]
+        public void AddOwnered_GetMetadata_Cannot_Be_Changed()
+        {
+            var p1 = new StyledProperty<string>(
+                "p1",
+                typeof(Class1),
+                typeof(Class1),
+                new StyledPropertyMetadata<string>());
+            var p2 = p1.AddOwner<Class2>();
+            var metadata = p2.GetMetadata<Class2>();
+
+            Assert.Throws<InvalidOperationException>(() => metadata.Merge(new StyledPropertyMetadata<string>(), p2));
+        }
+
         private class Class1 : AvaloniaObject
         {
         }