Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net8.0-windows10.0.17763.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net8.0-windows10.0.17763.0;net10.0;net10.0-windows10.0.17763.0</TargetFrameworks>
<!-- C# 13 enables the optimized lock statement for System.Threading.Lock (NET9+), where
the compiler emits Lock.EnterScope() instead of Monitor.Enter/Exit for reduced overhead. -->
<LangVersion>13.0</LangVersion>
</PropertyGroup>

<!--
Expand Down
25 changes: 25 additions & 0 deletions src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -36,7 +39,11 @@ public abstract class ObservableValidator : ObservableObject, INotifyDataErrorIn
/// so we need to replicate the same logic to retrieve the right display name for properties to validate and update that
/// property manually right before passing the context to <see cref="Validator"/> and proceed with the normal functionality.
/// </remarks>
#if NET8_0_OR_GREATER
private static readonly ConditionalWeakTable<Type, FrozenDictionary<string, string>> DisplayNamesMap = new();
#else
private static readonly ConditionalWeakTable<Type, Dictionary<string, string>> DisplayNamesMap = new();
#endif

/// <summary>
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
Expand Down Expand Up @@ -807,6 +814,23 @@ private void ClearErrorsForProperty(string propertyName)
[RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
private string GetDisplayNameForProperty(string propertyName)
{
#if NET8_0_OR_GREATER
static FrozenDictionary<string, string> GetDisplayNames(Type type)
{
Dictionary<string, string> displayNames = new();

foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (property.GetCustomAttribute<DisplayAttribute>() is DisplayAttribute attribute &&
attribute.GetName() is string displayName)
{
displayNames.Add(property.Name, displayName);
}
}

return displayNames.ToFrozenDictionary();
}
#else
static Dictionary<string, string> GetDisplayNames(Type type)
{
Dictionary<string, string> displayNames = new();
Expand All @@ -822,6 +846,7 @@ static Dictionary<string, string> GetDisplayNames(Type type)

return displayNames;
}
#endif

// This method replicates the logic of DisplayName and GetDisplayName from the
// ValidationContext class. See the original source in the BCL for more details.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ internal sealed class ConditionalWeakTable2<TKey, TValue>
/// <summary>
/// This lock protects all mutation of data in the table. Readers do not take this lock.
/// </summary>
private readonly object lockObject;
#if NET9_0_OR_GREATER
private readonly Lock lockObject = new();
#else
private readonly object lockObject = new();
#endif

/// <summary>
/// The actual storage for the table; swapped out as the table grows.
Expand All @@ -41,7 +45,6 @@ internal sealed class ConditionalWeakTable2<TKey, TValue>
/// </summary>
public ConditionalWeakTable2()
{
this.lockObject = new object();
this.container = new Container(this);
}

Expand Down Expand Up @@ -138,7 +141,11 @@ public Enumerator GetEnumerator()
// invoked. This is fine in this specific scenario because we're the only users of the enumerators so
// there's no concern about blocking other threads while enumerating. So here we just preemptively take
// a lock for the entire lifetime of the enumerator, and just release it once once we're done.
#if NET9_0_OR_GREATER
this.lockObject.Enter();
#else
Monitor.Enter(this.lockObject);
#endif

return new(this);
}
Expand Down Expand Up @@ -204,7 +211,11 @@ public Enumerator(ConditionalWeakTable2<TKey, TValue> table)
public void Dispose()
{
// Release the lock
#if NET9_0_OR_GREATER
this.table.lockObject.Exit();
#else
Monitor.Exit(this.table.lockObject);
#endif

this.table = null!;

Expand Down
25 changes: 19 additions & 6 deletions src/CommunityToolkit.Mvvm/Messaging/StrongReferenceMessenger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ public sealed class StrongReferenceMessenger : IMessenger
/// </remarks>
private readonly Dictionary2<Type2, IMapping> typesMap = new();

#if NET9_0_OR_GREATER
/// <summary>
/// The <see cref="Lock"/> used to synchronize access to <see cref="recipientsMap"/>.
/// </summary>
private readonly Lock recipientsMapLock = new();
#else
/// <summary>
/// The lock object used to synchronize access to <see cref="recipientsMap"/>.
/// </summary>
private readonly object recipientsMapLock = new();
#endif

/// <summary>
/// Gets the default <see cref="StrongReferenceMessenger"/> instance.
/// </summary>
Expand All @@ -108,7 +120,7 @@ public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
if (typeof(TToken) == typeof(Unit))
{
Expand Down Expand Up @@ -171,7 +183,7 @@ private void Register<TMessage, TToken>(object recipient, TToken token, MessageH
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Recipient key = new(recipient);
IMapping mapping;
Expand Down Expand Up @@ -227,7 +239,7 @@ public void UnregisterAll(object recipient)
{
ArgumentNullException.ThrowIfNull(recipient);

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
// If the recipient has no registered messages at all, ignore
Recipient key = new(recipient);
Expand Down Expand Up @@ -399,7 +411,7 @@ public void Unregister<TMessage, TToken>(object recipient, TToken token)
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
if (typeof(TToken) == typeof(Unit))
{
Expand Down Expand Up @@ -500,7 +512,7 @@ public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
Span<object?> pairs;
int i = 0;

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
if (typeof(TToken) == typeof(Unit))
{
Expand Down Expand Up @@ -618,7 +630,7 @@ void IMessenger.Cleanup()
/// <inheritdoc/>
public void Reset()
{
lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
this.recipientsMap.Clear();
this.typesMap.Clear();
Expand Down Expand Up @@ -856,3 +868,4 @@ private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
throw new InvalidOperationException("The target recipient has already subscribed to the target message.");
}
}

45 changes: 36 additions & 9 deletions src/CommunityToolkit.Mvvm/Messaging/WeakReferenceMessenger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ public sealed class WeakReferenceMessenger : IMessenger
/// </summary>
private readonly Dictionary2<Type2, ConditionalWeakTable2<object, object?>> recipientsMap = new();

#if NET9_0_OR_GREATER
/// <summary>
/// The <see cref="Lock"/> used to synchronize access to <see cref="recipientsMap"/>.
/// </summary>
private readonly Lock recipientsMapLock = new();
#else
/// <summary>
/// The lock object used to synchronize access to <see cref="recipientsMap"/>.
/// </summary>
private readonly object recipientsMapLock = new();
#endif

/// <summary>
/// Initializes a new instance of the <see cref="WeakReferenceMessenger"/> class.
/// </summary>
Expand Down Expand Up @@ -98,7 +110,7 @@ public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Type2 type2 = new(typeof(TMessage), typeof(TToken));

Expand Down Expand Up @@ -168,7 +180,7 @@ private void Register<TMessage, TToken>(object recipient, TToken token, MessageH
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Type2 type2 = new(typeof(TMessage), typeof(TToken));

Expand Down Expand Up @@ -209,7 +221,7 @@ public void UnregisterAll(object recipient)
{
ArgumentNullException.ThrowIfNull(recipient);

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Dictionary2<Type2, ConditionalWeakTable2<object, object?>>.Enumerator enumerator = this.recipientsMap.GetEnumerator();

Expand Down Expand Up @@ -237,7 +249,7 @@ public void UnregisterAll<TToken>(object recipient, TToken token)
throw new NotImplementedException();
}

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Dictionary2<Type2, ConditionalWeakTable2<object, object?>>.Enumerator enumerator = this.recipientsMap.GetEnumerator();

Expand Down Expand Up @@ -265,7 +277,7 @@ public void Unregister<TMessage, TToken>(object recipient, TToken token)
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Type2 type2 = new(typeof(TMessage), typeof(TToken));

Expand Down Expand Up @@ -296,7 +308,7 @@ public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
ArrayPoolBufferWriter<object?> bufferWriter;
int i = 0;

lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
Type2 type2 = new(typeof(TMessage), typeof(TToken));

Expand Down Expand Up @@ -422,7 +434,7 @@ internal static void SendAll<TMessage>(ReadOnlySpan<object?> pairs, int i, TMess
/// <inheritdoc/>
public void Cleanup()
{
lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
CleanupWithoutLock();
}
Expand All @@ -431,7 +443,7 @@ public void Cleanup()
/// <inheritdoc/>
public void Reset()
{
lock (this.recipientsMap)
lock (this.recipientsMapLock)
{
this.recipientsMap.Clear();
}
Expand All @@ -443,7 +455,20 @@ public void Reset()
/// </summary>
private void CleanupWithNonBlockingLock()
{
object lockObject = this.recipientsMap;
#if NET9_0_OR_GREATER
if (this.recipientsMapLock.TryEnter())
{
try
{
CleanupWithoutLock();
}
finally
{
this.recipientsMapLock.Exit();
}
}
#else
object lockObject = this.recipientsMapLock;
bool lockTaken = false;

try
Expand All @@ -462,6 +487,7 @@ private void CleanupWithNonBlockingLock()
Monitor.Exit(lockObject);
}
}
#endif
}

/// <summary>
Expand Down Expand Up @@ -546,3 +572,4 @@ private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
throw new InvalidOperationException("The target recipient has already subscribed to the target message.");
}
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net472;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net472;net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>14.0</LangVersion>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<DefineConstants>$(DefineConstants);ROSLYN_4_12_0_OR_GREATER;ROSLYN_5_0_0_OR_GREATER</DefineConstants>
Expand Down Expand Up @@ -32,4 +32,4 @@

<Import Project="..\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems" Label="Shared" />

</Project>
</Project>