diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs
new file mode 100644
index 00000000000..5566ed21b59
--- /dev/null
+++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.Toolkit.Mvvm.SourceGenerators
+{
+ ///
+ /// A source generator for necessary nullability attributes.
+ ///
+ [Generator]
+ public sealed class NullabilityAttributesGenerator : ISourceGenerator
+ {
+ ///
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ }
+
+ ///
+ public void Execute(GeneratorExecutionContext context)
+ {
+ AddSourceCodeIfTypeIsNotPresent(context, "System.Diagnostics.CodeAnalysis.NotNullAttribute");
+ AddSourceCodeIfTypeIsNotPresent(context, "System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute");
+ }
+
+ ///
+ /// Adds the source for a given attribute type if it's not present already in the compilation.
+ ///
+ private void AddSourceCodeIfTypeIsNotPresent(GeneratorExecutionContext context, string typeFullName)
+ {
+ if (context.Compilation.GetTypeByMetadataName(typeFullName) is not null)
+ {
+ return;
+ }
+
+ string
+ typeName = typeFullName.Split('.').Last(),
+ filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{typeName}.cs";
+
+ Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename);
+ StreamReader reader = new(stream);
+
+ string
+ originalSource = reader.ReadToEnd(),
+ outputSource = originalSource.Replace("NETSTANDARD2_0", "true");
+
+ context.AddSource($"{typeFullName}.cs", SourceText.From(outputSource, Encoding.UTF8));
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs
index 72d14de5394..3f9a4a7c8e7 100644
--- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs
+++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@@ -50,7 +51,7 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
///
/// The event is not raised if the current and new value for the target property are the same.
///
- protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer.Default.Equals(field, newValue))
{
@@ -75,7 +76,7 @@ protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string
/// The instance to use to compare the input values.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
- protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null)
{
if (comparer.Equals(field, newValue))
{
@@ -280,7 +281,7 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer<
/// is different than the previous one, and it does not mean the new
/// instance passed as argument is in any particular state.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName);
}
@@ -300,7 +301,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier,
///
/// The event is not raised if the current and new value for the target property are the same.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName);
}
@@ -338,7 +339,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier,
/// is different than the previous one, and it does not mean the new
/// instance passed as argument is in any particular state.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName);
}
@@ -359,7 +360,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti
///
/// The event is not raised if the current and new value for the target property are the same.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName);
}
diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj
index 8e718f68287..fa2804b3282 100644
--- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj
+++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj
@@ -9,6 +9,8 @@
+
+
@@ -17,6 +19,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
PreserveNewest
diff --git a/Microsoft.Toolkit.Mvvm/Attributes/NotNullAttribute.cs b/Microsoft.Toolkit.Mvvm/Attributes/NotNullAttribute.cs
new file mode 100644
index 00000000000..7725aeeeaf4
--- /dev/null
+++ b/Microsoft.Toolkit.Mvvm/Attributes/NotNullAttribute.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if NETSTANDARD2_0
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Specifies that an output will not be null even if the corresponding type allows it.
+ /// Specifies that an input argument was not null when the call returns.
+ ///
+ /// Internal copy from the BCL attribute.
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
+ internal sealed class NotNullAttribute : Attribute
+ {
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Mvvm/Attributes/NotNullIfNotNullAttribute.cs b/Microsoft.Toolkit.Mvvm/Attributes/NotNullIfNotNullAttribute.cs
new file mode 100644
index 00000000000..df828c4deec
--- /dev/null
+++ b/Microsoft.Toolkit.Mvvm/Attributes/NotNullIfNotNullAttribute.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if NETSTANDARD2_0
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Specifies that the output will be non-null if the named parameter is non-null.
+ ///
+ /// Internal copy from the BCL attribute.
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
+ internal sealed class NotNullIfNotNullAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
+ public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
+
+ /// Gets the associated parameter name.
+ public string ParameterName { get; }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
index 5685e3ed413..41fa934cfd6 100644
--- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
+++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
@@ -18,6 +18,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@@ -84,7 +85,7 @@ protected void OnPropertyChanging([CallerMemberName] string? propertyName = null
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
- protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null)
{
// We duplicate the code here instead of calling the overload because we can't
// guarantee that the invoked SetProperty will be inlined, and we need the JIT
@@ -120,7 +121,7 @@ protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string
/// The instance to use to compare the input values.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
- protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null)
{
if (comparer.Equals(field, newValue))
{
@@ -340,7 +341,7 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer<
/// indicates that the new value being assigned to is different than the previous one,
/// and it does not mean the new instance passed as argument is in any particular state.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
{
// We invoke the overload with a callback here to avoid code duplication, and simply pass an empty callback.
// The lambda expression here is transformed by the C# compiler into an empty closure class with a
@@ -368,7 +369,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier,
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName);
}
@@ -407,7 +408,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier,
/// indicates that the new value being assigned to is different than the previous one,
/// and it does not mean the new instance passed as argument is in any particular state.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName);
}
@@ -430,7 +431,7 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
- protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null)
+ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName);
}
diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs
index cbff751fea1..23e0d0bb6bb 100644
--- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs
+++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs
@@ -7,15 +7,9 @@
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
-// ================================= NOTE =================================
-// This file is mirrored in the ObservableRecipient annotated copy
-// (for debugging info) in the Mvvm.SourceGenerators project.
-// If any changes are made to this file, they should also be appropriately
-// ported to that file as well to keep the behavior consistent.
-// ========================================================================
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
@@ -145,7 +139,7 @@ protected virtual void Broadcast(T oldValue, T newValue, string? propertyName
/// the and events
/// are not raised if the current and new value for the target property are the same.
///
- protected bool SetProperty(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
{
T oldValue = field;
@@ -174,7 +168,7 @@ protected bool SetProperty(ref T field, T newValue, bool broadcast, [CallerMe
/// If , will also be invoked.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
- protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool broadcast, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, bool broadcast, [CallerMemberName] string? propertyName = null)
{
T oldValue = field;
diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs
index 4e25376064f..a21f36e3de8 100644
--- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs
+++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs
@@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
@@ -137,7 +138,7 @@ protected ObservableValidator(ValidationContext validationContext)
/// the and events
/// are not raised if the current and new value for the target property are the same.
///
- protected bool SetProperty(ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null)
{
bool propertyChanged = SetProperty(ref field, newValue, propertyName);
@@ -162,7 +163,7 @@ protected bool SetProperty(ref T field, T newValue, bool validate, [CallerMem
/// If , will also be validated.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
- protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool validate, [CallerMemberName] string? propertyName = null)
+ protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, IEqualityComparer comparer, bool validate, [CallerMemberName] string? propertyName = null)
{
bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);