Merge remote-tracking branch 'origin/master' into mhawker/color-picker-responsive
# Conflicts: # Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj # Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj
This commit is contained in:
Коммит
981b2881d1
|
@ -323,5 +323,5 @@ dotnet_diagnostic.SA1634.severity = none
|
|||
dotnet_diagnostic.SA1652.severity = none
|
||||
|
||||
dotnet_diagnostic.SA1629.severity = none # DocumentationTextMustEndWithAPeriod: Let's enable this rule back when we shift to WinUI3 (v8.x). If we do it now, it would mean more than 400 file changes.
|
||||
dotnet_diagnostic.SA1413.severity = none # UseTrailingCommasInMultiLineInitializers: This would also mean a lot of changes at the end of all multiline intializers. It's also debatable if we want this or not.
|
||||
dotnet_diagnostic.SA1413.severity = none # UseTrailingCommasInMultiLineInitializers: This would also mean a lot of changes at the end of all multiline initializers. It's also debatable if we want this or not.
|
||||
dotnet_diagnostic.SA1314.severity = none # TypeParameterNamesMustBeginWithT: We do have a few templates that don't start with T. We need to double check that changing this is not a breaking change. If not, we can re-enable this.
|
|
@ -7,10 +7,7 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||
-->
|
||||
<!-- 🚨 Please Do Not skip any instructions and information mentioned below as they are all required and essential to investigate the issue. Issues with missing information may be closed without investigation 🚨 -->
|
||||
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
@ -18,11 +15,17 @@ A clear and concise description of what the bug is.
|
|||
- [ ] Is this bug a regression in the toolkit? If so, what toolkit version did you last see it work:
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
- [ ] Can this be reproduced in the Sample App? (Either in a sample as-is or with new XAML pasted in the editor.) If so, please provide custom XAML or steps to reproduce. If not, let us know why it can't be reproduced (e.g. more complex setup, environment, dependencies, etc...) <!-- Being able to reproduce the problem in the sample app, really stream-lines the whole process in being able to discover, resolve, and validate bug fixes. -->
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
1. Given the following environment (Sample App w/ XAML, Project with Isolated setup, etc...)
|
||||
2. Go to '...'
|
||||
3. Click on '....'
|
||||
4. Scroll down to '....'
|
||||
5. See error
|
||||
|
||||
<!-- Provide as many code-snippets or XAML snippets where appropriate. -->
|
||||
|
||||
## Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
@ -42,6 +45,7 @@ Windows 10 Build Number:
|
|||
- [ ] April 2018 Update (17134)
|
||||
- [ ] October 2018 Update (17763)
|
||||
- [ ] May 2019 Update (18362)
|
||||
- [ ] May 2020 Update (19041)
|
||||
- [ ] Insider Build (build number: )
|
||||
|
||||
App min and target version:
|
||||
|
@ -49,6 +53,7 @@ App min and target version:
|
|||
- [ ] April 2018 Update (17134)
|
||||
- [ ] October 2018 Update (17763)
|
||||
- [ ] May 2019 Update (18362)
|
||||
- [ ] May 2020 Update (19041)
|
||||
- [ ] Insider Build (xxxxx)
|
||||
|
||||
Device form factor:
|
||||
|
|
|
@ -7,6 +7,8 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
<!-- 🚨 Please provide detailed information and Do Not skip any instructions as they are all required and essential to help us understand the feature 🚨 -->
|
||||
|
||||
## Describe the problem this feature would solve
|
||||
<!-- Please describe or link to any existing issues or discussions.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
|
|
@ -13,6 +13,7 @@ Hi!
|
|||
We try and keep our GitHub issue list for bugs and features.
|
||||
|
||||
Ideally, it'd be great to post your question on Stack Overflow using the 'windows-community-toolkit' tag here: https://stackoverflow.com/questions/tagged/windows-community-toolkit
|
||||
🚨 Please provide detailed information that includes examples, screenshots, and relevant issues if possible 🚨
|
||||
|
||||
If this is more about a scenario that you think is missing documentation, please file an issue instead at https://github.com/MicrosoftDocs/WindowsCommunityToolkitDocs/issues/new
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
## Fixes #<!-- Link to relevant issue (for ex: #1234) which will automatically close the issue once the PR is merged. -->
|
||||
<!-- 🚨 Please Do Not skip any instructions and information mentioned below as they are all required and essential to evaluate and test the PR. By fulfilling all the required information you will be able to reduce the volume of questions and most likely help merge the PR faster 🚨 -->
|
||||
|
||||
## Fixes #
|
||||
<!-- Add the relevant issue number after the "#" mentioned above (for ex: Fixes #1234) which will automatically close the issue once the PR is merged. -->
|
||||
|
||||
<!-- Add a brief overview here of the feature/bug & fix. -->
|
||||
|
||||
|
@ -37,7 +40,7 @@ Please check if your PR fulfills the following requirements:
|
|||
- [ ] Contains **NO** breaking changes
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below.
|
||||
Please note that breaking changes are likely to be rejected. -->
|
||||
Please note that breaking changes are likely to be rejected within minor release cycles or held until major versions. -->
|
||||
|
||||
|
||||
## Other information
|
||||
|
|
|
@ -227,4 +227,8 @@ msbuild.binlog
|
|||
!/build/tools/packages.config
|
||||
|
||||
# Generated file from .ttinclude
|
||||
**/Generated/TypeInfo.g.cs
|
||||
**/Generated/TypeInfo.g.cs
|
||||
|
||||
# TAEF Log output
|
||||
WexLogFileOutput
|
||||
*.wtl
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<IsTestProject>$(MSBuildProjectName.Contains('Test'))</IsTestProject>
|
||||
<IsUwpProject Condition="'$(IsDesignProject)' != 'true'">$(MSBuildProjectName.Contains('Uwp'))</IsUwpProject>
|
||||
<IsSampleProject>$(MSBuildProjectName.Contains('Sample'))</IsSampleProject>
|
||||
<DefaultTargetPlatformVersion>18362</DefaultTargetPlatformVersion>
|
||||
<DefaultTargetPlatformMinVersion>16299</DefaultTargetPlatformMinVersion>
|
||||
<DefaultTargetPlatformVersion>19041</DefaultTargetPlatformVersion>
|
||||
<DefaultTargetPlatformMinVersion>17763</DefaultTargetPlatformMinVersion>
|
||||
<PackageOutputPath>$(MSBuildThisFileDirectory)bin\nupkg</PackageOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<Project>
|
||||
<Choose>
|
||||
<When Condition="'$(TargetFramework)' == 'uap10.0' or '$(TargetFramework)' == 'uap10.0.16299' or '$(TargetFramework)' == 'native' or '$(TargetFramework)' == 'net461'">
|
||||
<When Condition="'$(TargetFramework)' == 'uap10.0' or '$(TargetFramework)' == 'uap10.0.17763' or '$(TargetFramework)' == 'native' or '$(TargetFramework)' == 'net461'">
|
||||
<!-- UAP versions for uap10.0 where TPMV isn't implied -->
|
||||
<PropertyGroup>
|
||||
<TargetPlatformVersion>10.0.$(DefaultTargetPlatformVersion).0</TargetPlatformVersion>
|
||||
|
@ -15,9 +15,6 @@
|
|||
<SDKReference Condition="'$(UseWindowsDesktopSdk)' == 'true' " Include="WindowsDesktop, Version=$(TargetPlatformVersion)">
|
||||
<Name>Windows Desktop Extensions for the UWP</Name>
|
||||
</SDKReference>
|
||||
<SDKReference Condition="'$(UseWindowsMobileSdk)' == 'true' " Include="WindowsMobile, Version=$(TargetPlatformVersion)">
|
||||
<Name>Windows Mobile Extensions for the UWP</Name>
|
||||
</SDKReference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<AssemblyName>GazeInputTest</AssemblyName>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.18362.0</TargetPlatformVersion>
|
||||
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.19041.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.17134.0</TargetPlatformMinVersion>
|
||||
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
|
@ -157,8 +157,8 @@
|
|||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Toolkit.UWP.Input.GazeInteraction\Microsoft.Toolkit.Uwp.Input.GazeInteraction.vcxproj">
|
||||
<Project>{a5e98964-45b1-442d-a07a-298a3221d81e}</Project>
|
||||
<ProjectReference Include="..\Microsoft.Toolkit.Uwp.Input.GazeInteraction\Microsoft.Toolkit.Uwp.Input.GazeInteraction.csproj">
|
||||
<Project>{5bf75694-798a-43a0-8150-415de195359c}</Project>
|
||||
<Name>Microsoft.Toolkit.Uwp.Input.GazeInteraction</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
</ToggleButton>
|
||||
<ProgressBar Grid.Row="1" x:Name="ProgressShow" Maximum="100" />
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" x:Name="DeviceAvailable" Text="Device availablility not yet detected"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" x:Name="DeviceAvailable" Text="Device availability not yet detected"/>
|
||||
<Grid Grid.Row="2" Grid.Column="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("GazeInputTest")]
|
||||
|
@ -20,13 +20,13 @@ using System.Runtime.InteropServices;
|
|||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
//[assembly: AssemblyVersion("1.0.0.0")]
|
||||
//[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
// [assembly: AssemblyVersion("1.0.0.0")]
|
||||
// [assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: ComVisible(false)]
|
|
@ -87,7 +87,7 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// <param name="obj">The input <see cref="object"/> instance, representing a boxed <typeparamref name="T"/> value.</param>
|
||||
/// <returns>A <see cref="Box{T}"/> reference pointing to <paramref name="obj"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method doesn't check the actual type of <paramref name="obj"/>, so it is responsability of the caller
|
||||
/// This method doesn't check the actual type of <paramref name="obj"/>, so it is responsibility of the caller
|
||||
/// to ensure it actually represents a boxed <typeparamref name="T"/> value and not some other instance.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
|
@ -125,7 +125,7 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator T(Box<T> box)
|
||||
{
|
||||
return Unsafe.Unbox<T>(box);
|
||||
return (T)(object)box;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -180,7 +180,6 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// <summary>
|
||||
/// Throws an <see cref="InvalidCastException"/> when a cast from an invalid <see cref="object"/> is attempted.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidCastExceptionForGetFrom()
|
||||
{
|
||||
throw new InvalidCastException($"Can't cast the input object to the type Box<{typeof(T)}>");
|
||||
|
|
|
@ -33,6 +33,11 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// </summary>
|
||||
private const int DefaultInitialBufferSize = 256;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
|
||||
/// </summary>
|
||||
private readonly ArrayPool<T> pool;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying <typeparamref name="T"/> array.
|
||||
/// </summary>
|
||||
|
@ -49,36 +54,52 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
|
||||
/// </summary>
|
||||
public ArrayPoolBufferWriter()
|
||||
: this(ArrayPool<T>.Shared, DefaultInitialBufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
|
||||
public ArrayPoolBufferWriter(ArrayPool<T> pool)
|
||||
: this(pool, DefaultInitialBufferSize)
|
||||
{
|
||||
// Since we're using pooled arrays, we can rent the buffer with the
|
||||
// default size immediately, we don't need to use lazy initialization
|
||||
// to save unnecessary memory allocations in this case.
|
||||
this.array = ArrayPool<T>.Shared.Rent(DefaultInitialBufferSize);
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="initialCapacity"/> is not positive (i.e. less than or equal to 0).
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="initialCapacity"/> is not valid.</exception>
|
||||
public ArrayPoolBufferWriter(int initialCapacity)
|
||||
: this(ArrayPool<T>.Shared, initialCapacity)
|
||||
{
|
||||
if (initialCapacity <= 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeExceptionForInitialCapacity();
|
||||
}
|
||||
}
|
||||
|
||||
this.array = ArrayPool<T>.Shared.Rent(initialCapacity);
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
|
||||
/// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="initialCapacity"/> is not valid.</exception>
|
||||
public ArrayPoolBufferWriter(ArrayPool<T> pool, int initialCapacity)
|
||||
{
|
||||
// Since we're using pooled arrays, we can rent the buffer with the
|
||||
// default size immediately, we don't need to use lazy initialization
|
||||
// to save unnecessary memory allocations in this case.
|
||||
// Additionally, we don't need to manually throw the exception if
|
||||
// the requested size is not valid, as that'll be thrown automatically
|
||||
// by the array pool in use when we try to rent an array with that size.
|
||||
this.pool = pool;
|
||||
this.array = pool.Rent(initialCapacity);
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
|
||||
/// </summary>
|
||||
~ArrayPoolBufferWriter() => this.Dispose();
|
||||
~ArrayPoolBufferWriter() => Dispose();
|
||||
|
||||
/// <inheritdoc/>
|
||||
Memory<T> IMemoryOwner<T>.Memory
|
||||
|
@ -182,6 +203,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
}
|
||||
|
||||
array.AsSpan(0, this.index).Clear();
|
||||
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
|
@ -250,7 +272,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
{
|
||||
int minimumSize = this.index + sizeHint;
|
||||
|
||||
ArrayPool<T>.Shared.Resize(ref this.array, minimumSize);
|
||||
this.pool.Resize(ref this.array, minimumSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,7 +290,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
|
||||
this.array = null;
|
||||
|
||||
ArrayPool<T>.Shared.Return(array);
|
||||
this.pool.Return(array);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -286,19 +308,9 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
return $"Microsoft.Toolkit.HighPerformance.Buffers.ArrayPoolBufferWriter<{typeof(T)}>[{this.index}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the initial capacity is invalid.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForInitialCapacity()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("initialCapacity", "The initial capacity must be a positive value");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForNegativeCount()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count", "The count can't be a negative value");
|
||||
|
@ -307,7 +319,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the size hint is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value");
|
||||
|
@ -316,7 +327,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForAdvancedTooFar()
|
||||
{
|
||||
throw new ArgumentException("The buffer writer has advanced too far");
|
||||
|
@ -325,7 +335,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> when <see cref="array"/> is <see langword="null"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowObjectDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException("The current buffer has already been disposed");
|
||||
|
|
|
@ -11,7 +11,7 @@ using System.Runtime.CompilerServices;
|
|||
namespace Microsoft.Toolkit.HighPerformance.Buffers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an utput sink into which <typeparamref name="T"/> data can be written, backed by a <see cref="Memory{T}"/> instance.
|
||||
/// Represents an output sink into which <typeparamref name="T"/> data can be written, backed by a <see cref="Memory{T}"/> instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items to write to the current instance.</typeparam>
|
||||
/// <remarks>
|
||||
|
@ -106,7 +106,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <inheritdoc/>
|
||||
public Memory<T> GetMemory(int sizeHint = 0)
|
||||
{
|
||||
this.ValidateSizeHint(sizeHint);
|
||||
ValidateSizeHint(sizeHint);
|
||||
|
||||
return this.memory.Slice(this.index);
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <inheritdoc/>
|
||||
public Span<T> GetSpan(int sizeHint = 0)
|
||||
{
|
||||
this.ValidateSizeHint(sizeHint);
|
||||
ValidateSizeHint(sizeHint);
|
||||
|
||||
return this.memory.Slice(this.index).Span;
|
||||
}
|
||||
|
@ -159,7 +159,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForNegativeCount()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count", "The count can't be a negative value");
|
||||
|
@ -168,7 +167,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the size hint is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value");
|
||||
|
@ -177,7 +175,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForAdvancedTooFar()
|
||||
{
|
||||
throw new ArgumentException("The buffer writer has advanced too far");
|
||||
|
@ -186,7 +183,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when the requested size exceeds the capacity.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForCapacityExceeded()
|
||||
{
|
||||
throw new ArgumentException("The buffer writer doesn't have enough capacity left");
|
||||
|
|
|
@ -32,6 +32,11 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
private readonly int length;
|
||||
#pragma warning restore IDE0032
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
|
||||
/// </summary>
|
||||
private readonly ArrayPool<T> pool;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying <typeparamref name="T"/> array.
|
||||
/// </summary>
|
||||
|
@ -41,12 +46,14 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// Initializes a new instance of the <see cref="MemoryOwner{T}"/> class with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="length">The length of the new memory buffer to use.</param>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
|
||||
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
|
||||
private MemoryOwner(int length, AllocationMode mode)
|
||||
private MemoryOwner(int length, ArrayPool<T> pool, AllocationMode mode)
|
||||
{
|
||||
this.start = 0;
|
||||
this.length = length;
|
||||
this.array = ArrayPool<T>.Shared.Rent(length);
|
||||
this.pool = pool;
|
||||
this.array = pool.Rent(length);
|
||||
|
||||
if (mode == AllocationMode.Clear)
|
||||
{
|
||||
|
@ -57,20 +64,22 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryOwner{T}"/> class with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="array">The input <typeparamref name="T"/> array to use.</param>
|
||||
/// <param name="start">The starting offset within <paramref name="array"/>.</param>
|
||||
/// <param name="length">The length of the array to use.</param>
|
||||
private MemoryOwner(T[] array, int start, int length)
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance currently in use.</param>
|
||||
/// <param name="array">The input <typeparamref name="T"/> array to use.</param>
|
||||
private MemoryOwner(int start, int length, ArrayPool<T> pool, T[] array)
|
||||
{
|
||||
this.start = start;
|
||||
this.length = length;
|
||||
this.pool = pool;
|
||||
this.array = array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="MemoryOwner{T}"/> class.
|
||||
/// </summary>
|
||||
~MemoryOwner() => this.Dispose();
|
||||
~MemoryOwner() => Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty <see cref="MemoryOwner{T}"/> instance.
|
||||
|
@ -79,7 +88,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
public static MemoryOwner<T> Empty
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new MemoryOwner<T>(0, AllocationMode.Default);
|
||||
get => new MemoryOwner<T>(0, ArrayPool<T>.Shared, AllocationMode.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -91,7 +100,19 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MemoryOwner<T> Allocate(int size) => new MemoryOwner<T>(size, AllocationMode.Default);
|
||||
public static MemoryOwner<T> Allocate(int size) => new MemoryOwner<T>(size, ArrayPool<T>.Shared, AllocationMode.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="size">The length of the new memory buffer to use.</param>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance currently in use.</param>
|
||||
/// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
|
||||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MemoryOwner<T> Allocate(int size, ArrayPool<T> pool) => new MemoryOwner<T>(size, pool, AllocationMode.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified parameters.
|
||||
|
@ -103,7 +124,20 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MemoryOwner<T> Allocate(int size, AllocationMode mode) => new MemoryOwner<T>(size, mode);
|
||||
public static MemoryOwner<T> Allocate(int size, AllocationMode mode) => new MemoryOwner<T>(size, ArrayPool<T>.Shared, mode);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="size">The length of the new memory buffer to use.</param>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance currently in use.</param>
|
||||
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
|
||||
/// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
|
||||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MemoryOwner<T> Allocate(int size, ArrayPool<T> pool, AllocationMode mode) => new MemoryOwner<T>(size, pool, mode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of items in the current instance
|
||||
|
@ -210,7 +244,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
ThrowInvalidLengthException();
|
||||
}
|
||||
|
||||
return new MemoryOwner<T>(array!, start, length);
|
||||
return new MemoryOwner<T>(start, length, this.pool, array!);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -227,7 +261,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
|
||||
this.array = null;
|
||||
|
||||
ArrayPool<T>.Shared.Return(array);
|
||||
this.pool.Return(array);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -251,7 +285,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> when <see cref="array"/> is <see langword="null"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowObjectDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(MemoryOwner<T>), "The current buffer has already been disposed");
|
||||
|
@ -260,7 +293,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="start"/> is invalid.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidOffsetException()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(start), "The input start parameter was not valid");
|
||||
|
@ -269,7 +301,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="length"/> is invalid.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidLengthException()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "The input length parameter was not valid");
|
||||
|
|
|
@ -42,6 +42,11 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
private readonly int length;
|
||||
#pragma warning restore IDE0032
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
|
||||
/// </summary>
|
||||
private readonly ArrayPool<T> pool;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying <typeparamref name="T"/> array.
|
||||
/// </summary>
|
||||
|
@ -51,11 +56,13 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// Initializes a new instance of the <see cref="SpanOwner{T}"/> struct with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="length">The length of the new memory buffer to use.</param>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
|
||||
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
|
||||
private SpanOwner(int length, AllocationMode mode)
|
||||
private SpanOwner(int length, ArrayPool<T> pool, AllocationMode mode)
|
||||
{
|
||||
this.length = length;
|
||||
this.array = ArrayPool<T>.Shared.Rent(length);
|
||||
this.pool = pool;
|
||||
this.array = pool.Rent(length);
|
||||
|
||||
if (mode == AllocationMode.Clear)
|
||||
{
|
||||
|
@ -70,7 +77,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
public static SpanOwner<T> Empty
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new SpanOwner<T>(0, AllocationMode.Default);
|
||||
get => new SpanOwner<T>(0, ArrayPool<T>.Shared, AllocationMode.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -82,7 +89,19 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SpanOwner<T> Allocate(int size) => new SpanOwner<T>(size, AllocationMode.Default);
|
||||
public static SpanOwner<T> Allocate(int size) => new SpanOwner<T>(size, ArrayPool<T>.Shared, AllocationMode.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="size">The length of the new memory buffer to use.</param>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
|
||||
/// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
|
||||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SpanOwner<T> Allocate(int size, ArrayPool<T> pool) => new SpanOwner<T>(size, pool, AllocationMode.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified parameters.
|
||||
|
@ -94,7 +113,20 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SpanOwner<T> Allocate(int size, AllocationMode mode) => new SpanOwner<T>(size, mode);
|
||||
public static SpanOwner<T> Allocate(int size, AllocationMode mode) => new SpanOwner<T>(size, ArrayPool<T>.Shared, mode);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="size">The length of the new memory buffer to use.</param>
|
||||
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
|
||||
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
|
||||
/// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
|
||||
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SpanOwner<T> Allocate(int size, ArrayPool<T> pool, AllocationMode mode) => new SpanOwner<T>(size, pool, mode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of items in the current instance
|
||||
|
@ -111,7 +143,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
public Span<T> Span
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new Span<T>(array, 0, this.length);
|
||||
get => new Span<T>(this.array, 0, this.length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -122,7 +154,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ref T DangerousGetReference()
|
||||
{
|
||||
return ref array.DangerousGetReference();
|
||||
return ref this.array.DangerousGetReference();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -131,7 +163,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Dispose()
|
||||
{
|
||||
ArrayPool<T>.Shared.Return(array);
|
||||
this.pool.Return(this.array);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -0,0 +1,826 @@
|
|||
// 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;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
#if NETCOREAPP3_1
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
#if !NETSTANDARD1_4
|
||||
using Microsoft.Toolkit.HighPerformance.Helpers;
|
||||
#endif
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Buffers
|
||||
{
|
||||
/// <summary>
|
||||
/// A configurable pool for <see cref="string"/> instances. This can be used to minimize allocations
|
||||
/// when creating multiple <see cref="string"/> instances from buffers of <see cref="char"/> values.
|
||||
/// The <see cref="GetOrAdd(ReadOnlySpan{char})"/> method provides a best-effort alternative to just creating
|
||||
/// a new <see cref="string"/> instance every time, in order to minimize the number of duplicated instances.
|
||||
/// The <see cref="StringPool"/> type will internally manage a highly efficient priority queue for the
|
||||
/// cached <see cref="string"/> instances, so that when the full capacity is reached, the least frequently
|
||||
/// used values will be automatically discarded to leave room for new values to cache.
|
||||
/// </summary>
|
||||
public sealed class StringPool
|
||||
{
|
||||
/// <summary>
|
||||
/// The size used by default by the parameterless constructor.
|
||||
/// </summary>
|
||||
private const int DefaultSize = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum size for <see cref="StringPool"/> instances.
|
||||
/// </summary>
|
||||
private const int MinimumSize = 32;
|
||||
|
||||
/// <summary>
|
||||
/// The current array of <see cref="FixedSizePriorityMap"/> instances in use.
|
||||
/// </summary>
|
||||
private readonly FixedSizePriorityMap[] maps;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of maps in use.
|
||||
/// </summary>
|
||||
private readonly int numberOfMaps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringPool"/> class.
|
||||
/// </summary>
|
||||
public StringPool()
|
||||
: this(DefaultSize)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringPool"/> class.
|
||||
/// </summary>
|
||||
/// <param name="minimumSize">The minimum size for the pool to create.</param>
|
||||
public StringPool(int minimumSize)
|
||||
{
|
||||
if (minimumSize <= 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
// Set the minimum size
|
||||
minimumSize = Math.Max(minimumSize, MinimumSize);
|
||||
|
||||
// Calculates the rounded up factors for a specific size/factor pair
|
||||
static void FindFactors(int size, int factor, out int x, out int y)
|
||||
{
|
||||
double
|
||||
a = Math.Sqrt((double)size / factor),
|
||||
b = factor * a;
|
||||
|
||||
x = RoundUpPowerOfTwo((int)a);
|
||||
y = RoundUpPowerOfTwo((int)b);
|
||||
}
|
||||
|
||||
// We want to find two powers of 2 factors that produce a number
|
||||
// that is at least equal to the requested size. In order to find the
|
||||
// combination producing the optimal factors (with the product being as
|
||||
// close as possible to the requested size), we test a number of ratios
|
||||
// that we consider acceptable, and pick the best results produced.
|
||||
// The ratio between maps influences the number of objects being allocated,
|
||||
// as well as the multithreading performance when locking on maps.
|
||||
// We still want to contraint this number to avoid situations where we
|
||||
// have a way too high number of maps compared to total size.
|
||||
FindFactors(minimumSize, 2, out int x2, out int y2);
|
||||
FindFactors(minimumSize, 3, out int x3, out int y3);
|
||||
FindFactors(minimumSize, 4, out int x4, out int y4);
|
||||
|
||||
int
|
||||
p2 = x2 * y2,
|
||||
p3 = x3 * y3,
|
||||
p4 = x4 * y4;
|
||||
|
||||
if (p3 < p2)
|
||||
{
|
||||
p2 = p3;
|
||||
x2 = x3;
|
||||
y2 = y3;
|
||||
}
|
||||
|
||||
if (p4 < p2)
|
||||
{
|
||||
p2 = p4;
|
||||
x2 = x4;
|
||||
y2 = y4;
|
||||
}
|
||||
|
||||
Span<FixedSizePriorityMap> span = this.maps = new FixedSizePriorityMap[x2];
|
||||
|
||||
// We preallocate the maps in advance, since each bucket only contains the
|
||||
// array field, which is not preinitialized, so the allocations are minimal.
|
||||
// This lets us lock on each individual maps when retrieving a string instance.
|
||||
foreach (ref FixedSizePriorityMap map in span)
|
||||
{
|
||||
map = new FixedSizePriorityMap(y2);
|
||||
}
|
||||
|
||||
this.numberOfMaps = x2;
|
||||
|
||||
Size = p2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds up an <see cref="int"/> value to a power of 2.
|
||||
/// </summary>
|
||||
/// <param name="x">The input value to round up.</param>
|
||||
/// <returns>The smallest power of two greater than or equal to <paramref name="x"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int RoundUpPowerOfTwo(int x)
|
||||
{
|
||||
#if NETCOREAPP3_1
|
||||
return 1 << (32 - BitOperations.LeadingZeroCount((uint)(x - 1)));
|
||||
#else
|
||||
x--;
|
||||
x |= x >> 1;
|
||||
x |= x >> 2;
|
||||
x |= x >> 4;
|
||||
x |= x >> 8;
|
||||
x |= x >> 16;
|
||||
x++;
|
||||
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared <see cref="StringPool"/> instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The shared pool provides a singleton, reusable <see cref="StringPool"/> instance that
|
||||
/// can be accessed directly, and that pools <see cref="string"/> instances for the entire
|
||||
/// process. Since <see cref="StringPool"/> is thread-safe, the shared instance can be used
|
||||
/// concurrently by multiple threads without the need for manual synchronization.
|
||||
/// </remarks>
|
||||
public static StringPool Shared { get; } = new StringPool();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of <see cref="string"/> that can be stored in the current instance.
|
||||
/// </summary>
|
||||
public int Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores a <see cref="string"/> instance in the internal cache.
|
||||
/// </summary>
|
||||
/// <param name="value">The input <see cref="string"/> instance to cache.</param>
|
||||
public void Add(string value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int
|
||||
hashcode = GetHashCode(value.AsSpan()),
|
||||
bucketIndex = hashcode & (this.numberOfMaps - 1);
|
||||
|
||||
ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);
|
||||
|
||||
lock (map.SyncRoot)
|
||||
{
|
||||
map.Add(value, hashcode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached <see cref="string"/> instance matching the input content, or stores the input one.
|
||||
/// </summary>
|
||||
/// <param name="value">The input <see cref="string"/> instance with the contents to use.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="value"/>, cached if possible.</returns>
|
||||
public string GetOrAdd(string value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
int
|
||||
hashcode = GetHashCode(value.AsSpan()),
|
||||
bucketIndex = hashcode & (this.numberOfMaps - 1);
|
||||
|
||||
ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);
|
||||
|
||||
lock (map.SyncRoot)
|
||||
{
|
||||
return map.GetOrAdd(value, hashcode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached <see cref="string"/> instance matching the input content, or creates a new one.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
|
||||
public string GetOrAdd(ReadOnlySpan<char> span)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
int
|
||||
hashcode = GetHashCode(span),
|
||||
bucketIndex = hashcode & (this.numberOfMaps - 1);
|
||||
|
||||
ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);
|
||||
|
||||
lock (map.SyncRoot)
|
||||
{
|
||||
return map.GetOrAdd(span, hashcode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached <see cref="string"/> instance matching the input content (converted to Unicode), or creates a new one.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use, in a specified encoding.</param>
|
||||
/// <param name="encoding">The <see cref="Encoding"/> instance to use to decode the contents of <paramref name="span"/>.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
|
||||
public unsafe string GetOrAdd(ReadOnlySpan<byte> span, Encoding encoding)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
int maxLength = encoding.GetMaxCharCount(span.Length);
|
||||
|
||||
using SpanOwner<char> buffer = SpanOwner<char>.Allocate(maxLength);
|
||||
|
||||
fixed (byte* source = span)
|
||||
fixed (char* destination = &buffer.DangerousGetReference())
|
||||
{
|
||||
int effectiveLength = encoding.GetChars(source, span.Length, destination, maxLength);
|
||||
|
||||
return GetOrAdd(new ReadOnlySpan<char>(destination, effectiveLength));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a cached <see cref="string"/> instance matching the input content, if present.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
|
||||
/// <param name="value">The resulting cached <see cref="string"/> instance, if present</param>
|
||||
/// <returns>Whether or not the target <see cref="string"/> instance was found.</returns>
|
||||
public bool TryGet(ReadOnlySpan<char> span, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
value = string.Empty;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
hashcode = GetHashCode(span),
|
||||
bucketIndex = hashcode & (this.numberOfMaps - 1);
|
||||
|
||||
ref FixedSizePriorityMap map = ref this.maps.DangerousGetReferenceAt(bucketIndex);
|
||||
|
||||
lock (map.SyncRoot)
|
||||
{
|
||||
return map.TryGet(span, hashcode, out value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the current instance and its associated maps.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
foreach (ref FixedSizePriorityMap map in this.maps.AsSpan())
|
||||
{
|
||||
lock (map.SyncRoot)
|
||||
{
|
||||
map.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configurable map containing a group of cached <see cref="string"/> instances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Instances of this type are stored in an array within <see cref="StringPool"/> and they are
|
||||
/// always accessed by reference - essentially as if this type had been a class. The type is
|
||||
/// also private, so there's no risk for users to directly access it and accidentally copy an
|
||||
/// instance, which would lead to bugs due to values becoming out of sync with the internal state
|
||||
/// (that is, because instances would be copied by value, so primitive fields would not be shared).
|
||||
/// The reason why we're using a struct here is to remove an indirection level and improve cache
|
||||
/// locality when accessing individual buckets from the methods in the <see cref="StringPool"/> type.
|
||||
/// </remarks>
|
||||
private struct FixedSizePriorityMap
|
||||
{
|
||||
/// <summary>
|
||||
/// The index representing the end of a given list.
|
||||
/// </summary>
|
||||
private const int EndOfList = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The array of 1-based indices for the <see cref="MapEntry"/> items stored in <see cref="mapEntries"/>.
|
||||
/// </summary>
|
||||
private readonly int[] buckets;
|
||||
|
||||
/// <summary>
|
||||
/// The array of currently cached entries (ie. the lists for each hash group).
|
||||
/// </summary>
|
||||
private readonly MapEntry[] mapEntries;
|
||||
|
||||
/// <summary>
|
||||
/// The array of priority values associated to each item stored in <see cref="mapEntries"/>.
|
||||
/// </summary>
|
||||
private readonly HeapEntry[] heapEntries;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of items stored in the map.
|
||||
/// </summary>
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// The current incremental timestamp for the items stored in <see cref="heapEntries"/>.
|
||||
/// </summary>
|
||||
private uint timestamp;
|
||||
|
||||
/// <summary>
|
||||
/// A type representing a map entry, ie. a node in a given list.
|
||||
/// </summary>
|
||||
private struct MapEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The precomputed hashcode for <see cref="Value"/>.
|
||||
/// </summary>
|
||||
public int HashCode;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="string"/> instance cached in this entry.
|
||||
/// </summary>
|
||||
public string? Value;
|
||||
|
||||
/// <summary>
|
||||
/// The 0-based index for the next node in the current list.
|
||||
/// </summary>
|
||||
public int NextIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The 0-based index for the heap entry corresponding to the current node.
|
||||
/// </summary>
|
||||
public int HeapIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A type representing a heap entry, used to associate priority to each item.
|
||||
/// </summary>
|
||||
private struct HeapEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The timestamp for the current entry (ie. the priority for the item).
|
||||
/// </summary>
|
||||
public uint Timestamp;
|
||||
|
||||
/// <summary>
|
||||
/// The 0-based index for the map entry corresponding to the current item.
|
||||
/// </summary>
|
||||
public int MapIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixedSizePriorityMap"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The fixed capacity of the current map.</param>
|
||||
public FixedSizePriorityMap(int capacity)
|
||||
{
|
||||
this.buckets = new int[capacity];
|
||||
this.mapEntries = new MapEntry[capacity];
|
||||
this.heapEntries = new HeapEntry[capacity];
|
||||
this.count = 0;
|
||||
this.timestamp = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="object"/> that can be used to synchronize access to the current instance.
|
||||
/// </summary>
|
||||
public object SyncRoot
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.buckets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="StringPool.Add"/> for the current instance.
|
||||
/// </summary>
|
||||
/// <param name="value">The input <see cref="string"/> instance to cache.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void Add(string value, int hashcode)
|
||||
{
|
||||
ref string target = ref TryGet(value.AsSpan(), hashcode);
|
||||
|
||||
if (Unsafe.AreSame(ref target, ref Unsafe.AsRef<string>(null)))
|
||||
{
|
||||
Insert(value, hashcode);
|
||||
}
|
||||
else
|
||||
{
|
||||
target = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="StringPool.GetOrAdd(string)"/> for the current instance.
|
||||
/// </summary>
|
||||
/// <param name="value">The input <see cref="string"/> instance with the contents to use.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="value"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe string GetOrAdd(string value, int hashcode)
|
||||
{
|
||||
ref string result = ref TryGet(value.AsSpan(), hashcode);
|
||||
|
||||
if (!Unsafe.AreSame(ref result, ref Unsafe.AsRef<string>(null)))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
Insert(value, hashcode);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="StringPool.GetOrAdd(ReadOnlySpan{char})"/> for the current instance.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
|
||||
/// <returns>A <see cref="string"/> instance with the contents of <paramref name="span"/>, cached if possible.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe string GetOrAdd(ReadOnlySpan<char> span, int hashcode)
|
||||
{
|
||||
ref string result = ref TryGet(span, hashcode);
|
||||
|
||||
if (!Unsafe.AreSame(ref result, ref Unsafe.AsRef<string>(null)))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
string value = span.ToString();
|
||||
|
||||
Insert(value, hashcode);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="StringPool.TryGet"/> for the current instance.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
|
||||
/// <param name="value">The resulting cached <see cref="string"/> instance, if present</param>
|
||||
/// <returns>Whether or not the target <see cref="string"/> instance was found.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe bool TryGet(ReadOnlySpan<char> span, int hashcode, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
ref string result = ref TryGet(span, hashcode);
|
||||
|
||||
if (!Unsafe.AreSame(ref result, ref Unsafe.AsRef<string>(null)))
|
||||
{
|
||||
value = result;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the current instance and throws away all the cached values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
this.buckets.AsSpan().Clear();
|
||||
this.mapEntries.AsSpan().Clear();
|
||||
this.heapEntries.AsSpan().Clear();
|
||||
this.count = 0;
|
||||
this.timestamp = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a target <see cref="string"/> instance, if it exists, and returns a reference to it.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> with the contents to use.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="span"/>.</param>
|
||||
/// <returns>A reference to the slot where the target <see cref="string"/> instance could be.</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe ref string TryGet(ReadOnlySpan<char> span, int hashcode)
|
||||
{
|
||||
ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
|
||||
ref MapEntry entry = ref Unsafe.AsRef<MapEntry>(null);
|
||||
int
|
||||
length = this.buckets.Length,
|
||||
bucketIndex = hashcode & (length - 1);
|
||||
|
||||
for (int i = this.buckets.DangerousGetReferenceAt(bucketIndex) - 1;
|
||||
(uint)i < (uint)length;
|
||||
i = entry.NextIndex)
|
||||
{
|
||||
entry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)i);
|
||||
|
||||
if (entry.HashCode == hashcode &&
|
||||
entry.Value!.AsSpan().SequenceEqual(span))
|
||||
{
|
||||
UpdateTimestamp(ref entry.HeapIndex);
|
||||
|
||||
return ref entry.Value!;
|
||||
}
|
||||
}
|
||||
|
||||
return ref Unsafe.AsRef<string>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new <see cref="string"/> instance in the current map, freeing up a space if needed.
|
||||
/// </summary>
|
||||
/// <param name="value">The new <see cref="string"/> instance to store.</param>
|
||||
/// <param name="hashcode">The precomputed hashcode for <paramref name="value"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void Insert(string value, int hashcode)
|
||||
{
|
||||
ref int bucketsRef = ref this.buckets.DangerousGetReference();
|
||||
ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
|
||||
ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference();
|
||||
int entryIndex, heapIndex;
|
||||
|
||||
// If the current map is full, first get the oldest value, which is
|
||||
// always the first item in the heap. Then, free up a slot by
|
||||
// removing that, and insert the new value in that empty location.
|
||||
if (this.count == this.mapEntries.Length)
|
||||
{
|
||||
entryIndex = heapEntriesRef.MapIndex;
|
||||
heapIndex = 0;
|
||||
|
||||
ref MapEntry removedEntry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)entryIndex);
|
||||
|
||||
// The removal logic can be extremely optimized in this case, as we
|
||||
// can retrieve the precomputed hashcode for the target entry by doing
|
||||
// a lookup on the target map node, and we can also skip all the comparisons
|
||||
// while traversing the target chain since we know in advance the index of
|
||||
// the target node which will contain the item to remove from the map.
|
||||
Remove(removedEntry.HashCode, entryIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the free list is not empty, get that map node and update the field
|
||||
entryIndex = this.count;
|
||||
heapIndex = this.count;
|
||||
}
|
||||
|
||||
int bucketIndex = hashcode & (this.buckets.Length - 1);
|
||||
ref int targetBucket = ref Unsafe.Add(ref bucketsRef, (IntPtr)(void*)(uint)bucketIndex);
|
||||
ref MapEntry targetMapEntry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)entryIndex);
|
||||
ref HeapEntry targetHeapEntry = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)heapIndex);
|
||||
|
||||
// Assign the values in the new map entry
|
||||
targetMapEntry.HashCode = hashcode;
|
||||
targetMapEntry.Value = value;
|
||||
targetMapEntry.NextIndex = targetBucket - 1;
|
||||
targetMapEntry.HeapIndex = heapIndex;
|
||||
|
||||
// Update the bucket slot and the current count
|
||||
targetBucket = entryIndex + 1;
|
||||
this.count++;
|
||||
|
||||
// Link the heap node with the current entry
|
||||
targetHeapEntry.MapIndex = entryIndex;
|
||||
|
||||
// Update the timestamp and balance the heap again
|
||||
UpdateTimestamp(ref targetMapEntry.HeapIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a specified <see cref="string"/> instance from the map to free up one slot.
|
||||
/// </summary>
|
||||
/// <param name="hashcode">The precomputed hashcode of the instance to remove.</param>
|
||||
/// <param name="mapIndex">The index of the target map node to remove.</param>
|
||||
/// <remarks>The input <see cref="string"/> instance needs to already exist in the map.</remarks>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void Remove(int hashcode, int mapIndex)
|
||||
{
|
||||
ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
|
||||
int
|
||||
bucketIndex = hashcode & (this.buckets.Length - 1),
|
||||
entryIndex = this.buckets.DangerousGetReferenceAt(bucketIndex) - 1,
|
||||
lastIndex = EndOfList;
|
||||
|
||||
// We can just have an undefined loop, as the input
|
||||
// value we're looking for is guaranteed to be present
|
||||
while (true)
|
||||
{
|
||||
ref MapEntry candidate = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)entryIndex);
|
||||
|
||||
// Check the current value for a match
|
||||
if (entryIndex == mapIndex)
|
||||
{
|
||||
// If this was not the first list node, update the parent as well
|
||||
if (lastIndex != EndOfList)
|
||||
{
|
||||
ref MapEntry lastEntry = ref Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)lastIndex);
|
||||
|
||||
lastEntry.NextIndex = candidate.NextIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, update the target index from the bucket slot
|
||||
this.buckets.DangerousGetReferenceAt(bucketIndex) = candidate.NextIndex + 1;
|
||||
}
|
||||
|
||||
this.count--;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to the following node in the current list
|
||||
lastIndex = entryIndex;
|
||||
entryIndex = candidate.NextIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the timestamp of a heap node at the specified index (which is then synced back).
|
||||
/// </summary>
|
||||
/// <param name="heapIndex">The index of the target heap node to update.</param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void UpdateTimestamp(ref int heapIndex)
|
||||
{
|
||||
int
|
||||
currentIndex = heapIndex,
|
||||
count = this.count;
|
||||
ref MapEntry mapEntriesRef = ref this.mapEntries.DangerousGetReference();
|
||||
ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference();
|
||||
ref HeapEntry root = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)currentIndex);
|
||||
uint timestamp = this.timestamp;
|
||||
|
||||
// Check if incrementing the current timestamp for the heap node to update
|
||||
// would result in an overflow. If that happened, we could end up violating
|
||||
// the min-heap property (the value of each node has to always be <= than that
|
||||
// of its child nodes), eg. if we were updating a node that was not the root.
|
||||
// In that scenario, we could end up with a node somewhere along the heap with
|
||||
// a value lower than that of its parent node (as the timestamp would be 0).
|
||||
// To guard against this, we just check the current timestamp value, and if
|
||||
// the maximum value has been reached, we reinitialize the entire heap. This
|
||||
// is done in a non-inlined call, so we don't increase the codegen size in this
|
||||
// method. The reinitialization simply traverses the heap in breadth-first order
|
||||
// (ie. level by level), and assigns incrementing timestamps to all nodes starting
|
||||
// from 0. The value of the current timestamp is then just set to the current size.
|
||||
if (timestamp == uint.MaxValue)
|
||||
{
|
||||
// We use a goto here as this path is very rarely taken. Doing so
|
||||
// causes the generated asm to contain a forward jump to the fallback
|
||||
// path if this branch is taken, whereas the normal execution path will
|
||||
// not need to execute any jumps at all. This is done to reduce the overhead
|
||||
// introduced by this check in all the invocations where this point is not reached.
|
||||
goto Fallback;
|
||||
}
|
||||
|
||||
Downheap:
|
||||
|
||||
// Assign a new timestamp to the target heap node. We use a
|
||||
// local incremental timestamp instead of using the system timer
|
||||
// as this greatly reduces the overhead and the time spent in system calls.
|
||||
// The uint type provides a large enough range and it's unlikely users would ever
|
||||
// exhaust it anyway (especially considering each map has a separate counter).
|
||||
root.Timestamp = this.timestamp = timestamp + 1;
|
||||
|
||||
// Once the timestamp is updated (which will cause the heap to become
|
||||
// unbalanced), start a sift down loop to balance the heap again.
|
||||
while (true)
|
||||
{
|
||||
// The heap is 0-based (so that the array length can remain the same
|
||||
// as the power of 2 value used for the other arrays in this type).
|
||||
// This means that children of each node are at positions:
|
||||
// - left: (2 * n) + 1
|
||||
// - right: (2 * n) + 2
|
||||
ref HeapEntry minimum = ref root;
|
||||
int
|
||||
left = (currentIndex * 2) + 1,
|
||||
right = (currentIndex * 2) + 2,
|
||||
targetIndex = currentIndex;
|
||||
|
||||
// Check and update the left child, if necessary
|
||||
if (left < count)
|
||||
{
|
||||
ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)left);
|
||||
|
||||
if (child.Timestamp < minimum.Timestamp)
|
||||
{
|
||||
minimum = ref child;
|
||||
targetIndex = left;
|
||||
}
|
||||
}
|
||||
|
||||
// Same check as above for the right child
|
||||
if (right < count)
|
||||
{
|
||||
ref HeapEntry child = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)right);
|
||||
|
||||
if (child.Timestamp < minimum.Timestamp)
|
||||
{
|
||||
minimum = ref child;
|
||||
targetIndex = right;
|
||||
}
|
||||
}
|
||||
|
||||
// If no swap is pending, we can just stop here.
|
||||
// Before returning, we update the target index as well.
|
||||
if (Unsafe.AreSame(ref root, ref minimum))
|
||||
{
|
||||
heapIndex = targetIndex;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the indices in the respective map entries (accounting for the swap)
|
||||
Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)root.MapIndex).HeapIndex = targetIndex;
|
||||
Unsafe.Add(ref mapEntriesRef, (IntPtr)(void*)(uint)minimum.MapIndex).HeapIndex = currentIndex;
|
||||
|
||||
currentIndex = targetIndex;
|
||||
|
||||
// Swap the parent and child (so that the minimum value bubbles up)
|
||||
HeapEntry temp = root;
|
||||
|
||||
root = minimum;
|
||||
minimum = temp;
|
||||
|
||||
// Update the reference to the root node
|
||||
root = ref Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)currentIndex);
|
||||
}
|
||||
|
||||
Fallback:
|
||||
|
||||
UpdateAllTimestamps();
|
||||
|
||||
// After having updated all the timestamps, if the heap contains N items, the
|
||||
// node in the bottom right corner will have a value of N - 1. Since the timestamp
|
||||
// is incremented by 1 before starting the downheap execution, here we simply
|
||||
// update the local timestamp to be N - 1, so that the code above will set the
|
||||
// timestamp of the node currently being updated to exactly N.
|
||||
timestamp = (uint)(count - 1);
|
||||
|
||||
goto Downheap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the timestamp of all the current heap nodes in incrementing order.
|
||||
/// The heap is always guaranteed to be complete binary tree, so when it contains
|
||||
/// a given number of nodes, those are all contiguous from the start of the array.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void UpdateAllTimestamps()
|
||||
{
|
||||
int count = this.count;
|
||||
ref HeapEntry heapEntriesRef = ref this.heapEntries.DangerousGetReference();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Unsafe.Add(ref heapEntriesRef, (IntPtr)(void*)(uint)i).Timestamp = (uint)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the (positive) hashcode for a given <see cref="ReadOnlySpan{T}"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> instance.</param>
|
||||
/// <returns>The hashcode for <paramref name="span"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int GetHashCode(ReadOnlySpan<char> span)
|
||||
{
|
||||
#if NETSTANDARD1_4
|
||||
return span.GetDjb2HashCode();
|
||||
#else
|
||||
return HashCode<char>.Combine(span);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when the requested size exceeds the capacity.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentOutOfRangeException()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("minimumSize", "The requested size must be greater than 0");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -205,7 +205,6 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="column"/> is invalid.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForInvalidColumn()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "The target column parameter was not valid");
|
||||
|
|
|
@ -160,7 +160,6 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="row"/> is invalid.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForInvalidRow()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(row), "The target row parameter was not valid");
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
|
@ -42,8 +43,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="ReadOnlySpanEnumerable{T}"/> instance targeting the current <see cref="ReadOnlySpan{T}"/> value.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpanEnumerable<T> GetEnumerator() => this;
|
||||
public readonly ReadOnlySpanEnumerable<T> GetEnumerator() => this;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
|
||||
|
@ -67,7 +69,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// <summary>
|
||||
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
|
||||
/// </summary>
|
||||
public Item Current
|
||||
public readonly Item Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
||||
|
@ -54,8 +55,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="ReadOnlySpanTokenizer{T}"/> instance targeting the current <see cref="ReadOnlySpan{T}"/> value.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpanTokenizer<T> GetEnumerator() => this;
|
||||
public readonly ReadOnlySpanTokenizer<T> GetEnumerator() => this;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
|
||||
|
@ -94,7 +96,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// <summary>
|
||||
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<T> Current
|
||||
public readonly ReadOnlySpan<T> Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.span.Slice(this.start, this.end - this.start);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
|
@ -42,8 +43,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="SpanEnumerable{T}"/> instance targeting the current <see cref="Span{T}"/> value.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SpanEnumerable<T> GetEnumerator() => this;
|
||||
public readonly SpanEnumerable<T> GetEnumerator() => this;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
|
||||
|
@ -67,7 +69,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// <summary>
|
||||
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
|
||||
/// </summary>
|
||||
public Item Current
|
||||
public readonly Item Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
|
@ -76,12 +78,12 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
ref T r0 = ref MemoryMarshal.GetReference(this.span);
|
||||
ref T ri = ref Unsafe.Add(ref r0, this.index);
|
||||
|
||||
// On .NET Standard 2.1 we can save 4 bytes by piggybacking
|
||||
// the current index in the length of the wrapped span.
|
||||
// We're going to use the first item as the target reference,
|
||||
// and the length as a host for the current original offset.
|
||||
// This is not possible on .NET Standard 2.1 as we lack
|
||||
// the API to create spans from arbitrary references.
|
||||
// On .NET Standard 2.1 and .NET Core (or on any target that offers runtime
|
||||
// support for the Span<T> types), we can save 4 bytes by piggybacking the
|
||||
// current index in the length of the wrapped span. We're going to use the
|
||||
// first item as the target reference, and the length as a host for the
|
||||
// current original offset. This is not possible on eg. .NET Standard 2.0,
|
||||
// as we lack the API to create Span<T>-s from arbitrary references.
|
||||
return new Item(ref ri, this.index);
|
||||
#else
|
||||
return new Item(this.span, this.index);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
||||
|
@ -54,8 +55,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="SpanTokenizer{T}"/> instance targeting the current <see cref="Span{T}"/> value.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SpanTokenizer<T> GetEnumerator() => this;
|
||||
public readonly SpanTokenizer<T> GetEnumerator() => this;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
|
||||
|
@ -94,7 +96,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
|
|||
/// <summary>
|
||||
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
|
||||
/// </summary>
|
||||
public Span<T> Current
|
||||
public readonly Span<T> Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.span.Slice(this.start, this.end - this.start);
|
||||
|
|
|
@ -61,18 +61,18 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>
|
||||
/// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the <paramref name="i"/>
|
||||
/// and <paramref name="j"/> parameters are valid. Furthermore, this extension will ignore the lower bounds for the input
|
||||
/// array, and will just assume that the input index is 0-based. It is responsability of the caller to adjust the input
|
||||
/// array, and will just assume that the input index is 0-based. It is responsibility of the caller to adjust the input
|
||||
/// indices to account for the actual lower bounds, if the input array has either axis not starting at 0.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T DangerousGetReferenceAt<T>(this T[,] array, int i, int j)
|
||||
public static unsafe ref T DangerousGetReferenceAt<T>(this T[,] array, int i, int j)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArray2DData>(array);
|
||||
int offset = (i * arrayData.Width) + j;
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
ref T ri = ref Unsafe.Add(ref r0, offset);
|
||||
ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)offset);
|
||||
|
||||
return ref ri;
|
||||
#else
|
||||
|
@ -82,10 +82,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
return ref array[i, j];
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
return ref Unsafe.AsRef<T>(null);
|
||||
}
|
||||
return ref Unsafe.AsRef<T>(null);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -234,7 +231,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
/// <summary>
|
||||
/// Cretes a new <see cref="Span{T}"/> over an input 2D <typeparamref name="T"/> array.
|
||||
/// Creates a new <see cref="Span{T}"/> over an input 2D <typeparamref name="T"/> array.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the input 2D <typeparamref name="T"/> array instance.</typeparam>
|
||||
/// <param name="array">The input 2D <typeparamref name="T"/> array instance.</param>
|
||||
|
@ -274,11 +271,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <returns>The number of occurrences of <paramref name="value"/> in <paramref name="array"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Count<T>(this T[,] array, T value)
|
||||
public static unsafe int Count<T>(this T[,] array, T value)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
ref T r0 = ref array.DangerousGetReference();
|
||||
IntPtr length = (IntPtr)array.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)array.Length;
|
||||
|
||||
return SpanHelper.Count(ref r0, length, value);
|
||||
}
|
||||
|
@ -293,11 +290,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>The Djb2 hash is fully deterministic and with no random components.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetDjb2HashCode<T>(this T[,] array)
|
||||
public static unsafe int GetDjb2HashCode<T>(this T[,] array)
|
||||
where T : notnull
|
||||
{
|
||||
ref T r0 = ref array.DangerousGetReference();
|
||||
IntPtr length = (IntPtr)array.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)array.Length;
|
||||
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, length);
|
||||
}
|
||||
|
|
|
@ -62,12 +62,12 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the <paramref name="i"/> parameter is valid.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T DangerousGetReferenceAt<T>(this T[] array, int i)
|
||||
public static unsafe ref T DangerousGetReferenceAt<T>(this T[] array, int i)
|
||||
{
|
||||
#if NETCORE_RUNTIME
|
||||
var arrayData = Unsafe.As<RawArrayData>(array);
|
||||
ref T r0 = ref Unsafe.As<byte, T>(ref arrayData.Data);
|
||||
ref T ri = ref Unsafe.Add(ref r0, i);
|
||||
ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i);
|
||||
|
||||
return ref ri;
|
||||
#else
|
||||
|
@ -76,10 +76,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
return ref array[i];
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
return ref Unsafe.AsRef<T>(null);
|
||||
}
|
||||
return ref Unsafe.AsRef<T>(null);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -114,11 +111,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <returns>The number of occurrences of <paramref name="value"/> in <paramref name="array"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Count<T>(this T[] array, T value)
|
||||
public static unsafe int Count<T>(this T[] array, T value)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
ref T r0 = ref array.DangerousGetReference();
|
||||
IntPtr length = (IntPtr)array.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)array.Length;
|
||||
|
||||
return SpanHelper.Count(ref r0, length, value);
|
||||
}
|
||||
|
@ -185,11 +182,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>The Djb2 hash is fully deterministic and with no random components.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetDjb2HashCode<T>(this T[] array)
|
||||
public static unsafe int GetDjb2HashCode<T>(this T[] array)
|
||||
where T : notnull
|
||||
{
|
||||
ref T r0 = ref array.DangerousGetReference();
|
||||
IntPtr length = (IntPtr)array.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)array.Length;
|
||||
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, length);
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
T[] newArray = pool.Rent(newSize);
|
||||
int itemsToCopy = Math.Min(array.Length, newSize);
|
||||
|
||||
array.AsSpan(0, itemsToCopy).CopyTo(newArray);
|
||||
Array.Copy(array, 0, newArray, 0, itemsToCopy);
|
||||
|
||||
pool.Return(array, clearArray);
|
||||
|
||||
|
|
|
@ -102,7 +102,6 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target writer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForEndOfBuffer()
|
||||
{
|
||||
throw new ArgumentException("The current buffer writer can't contain the requested input data.");
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
// 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;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Streams;
|
||||
using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Extensions
|
||||
{
|
||||
|
@ -18,17 +19,18 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <summary>
|
||||
/// Returns a <see cref="Stream"/> wrapping the contents of the given <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="memory">The input <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance.</param>
|
||||
/// <returns>A <see cref="Stream"/> wrapping the data within <paramref name="memory"/>.</returns>
|
||||
/// <param name="memoryOwner">The input <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance.</param>
|
||||
/// <returns>A <see cref="Stream"/> wrapping the data within <paramref name="memoryOwner"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The caller does not need to track the lifetime of the input <see cref="IMemoryOwner{T}"/> of <see cref="byte"/>
|
||||
/// instance, as the returned <see cref="Stream"/> will take care of disposing that buffer when it is closed.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="memoryOwner"/> has an invalid data store.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Stream AsStream(this IMemoryOwner<byte> memory)
|
||||
public static Stream AsStream(this IMemoryOwner<byte> memoryOwner)
|
||||
{
|
||||
return new IMemoryOwnerStream(memory);
|
||||
return MemoryStream.Create(memoryOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,15 +22,16 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <returns>A <see cref="Stream"/> wrapping the data within <paramref name="memory"/>.</returns>
|
||||
/// <remarks>
|
||||
/// Since this method only receives a <see cref="Memory{T}"/> instance, which does not track
|
||||
/// the lifetime of its underlying buffer, it is responsability of the caller to manage that.
|
||||
/// the lifetime of its underlying buffer, it is responsibility of the caller to manage that.
|
||||
/// In particular, the caller must ensure that the target buffer is not disposed as long
|
||||
/// as the returned <see cref="Stream"/> is in use, to avoid unexpected issues.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="memory"/> has an invalid data store.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Stream AsStream(this Memory<byte> memory)
|
||||
{
|
||||
return new MemoryStream(memory);
|
||||
return MemoryStream.Create(memory, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// The <see cref="IntPtr"/> value representing the offset to the target field from the start of the object data
|
||||
/// for the parameter <paramref name="obj"/>. The offset is in relation to the first usable byte after the method table.
|
||||
/// </returns>
|
||||
/// <remarks>The input parameters are not validated, and it's responsability of the caller to ensure that
|
||||
/// <remarks>The input parameters are not validated, and it's responsibility of the caller to ensure that
|
||||
/// the <paramref name="data"/> reference is actually pointing to a memory location within <paramref name="obj"/>.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
|
@ -46,7 +46,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <param name="offset">The input byte offset for the <typeparamref name="T"/> reference to retrieve.</param>
|
||||
/// <returns>A <typeparamref name="T"/> reference at a specified offset within <paramref name="obj"/>.</returns>
|
||||
/// <remarks>
|
||||
/// None of the input arguments is validated, and it is responsability of the caller to ensure they are valid.
|
||||
/// None of the input arguments is validated, and it is responsibility of the caller to ensure they are valid.
|
||||
/// In particular, using an invalid offset might cause the retrieved reference to be misaligned with the
|
||||
/// desired data, which would break the type system. Or, if the offset causes the retrieved reference to point
|
||||
/// to a memory location outside of the input <see cref="object"/> instance, that might lead to runtime crashes.
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using MemoryStream = Microsoft.Toolkit.HighPerformance.Streams.MemoryStream;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Extensions
|
||||
|
@ -22,15 +21,15 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <returns>A <see cref="Stream"/> wrapping the data within <paramref name="memory"/>.</returns>
|
||||
/// <remarks>
|
||||
/// Since this method only receives a <see cref="Memory{T}"/> instance, which does not track
|
||||
/// the lifetime of its underlying buffer, it is responsability of the caller to manage that.
|
||||
/// the lifetime of its underlying buffer, it is responsibility of the caller to manage that.
|
||||
/// In particular, the caller must ensure that the target buffer is not disposed as long
|
||||
/// as the returned <see cref="Stream"/> is in use, to avoid unexpected issues.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="memory"/> has an invalid data store.</exception>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Stream AsStream(this ReadOnlyMemory<byte> memory)
|
||||
{
|
||||
return new MemoryStream(memory);
|
||||
return MemoryStream.Create(memory, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,40 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the <paramref name="i"/> parameter is valid.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T DangerousGetReferenceAt<T>(this ReadOnlySpan<T> span, int i)
|
||||
public static unsafe ref T DangerousGetReferenceAt<T>(this ReadOnlySpan<T> span, int i)
|
||||
{
|
||||
// Here we assume the input index will never be negative, so we do an unsafe cast to
|
||||
// force the JIT to skip the sign extension when going from int to native int.
|
||||
// On .NET Core 3.1, if we only use Unsafe.Add(ref r0, i), we get the following:
|
||||
// =============================
|
||||
// L0000: mov rax, [rcx]
|
||||
// L0003: movsxd rdx, edx
|
||||
// L0006: lea rax, [rax+rdx*4]
|
||||
// L000a: ret
|
||||
// =============================
|
||||
// Note the movsxd (move with sign extension) to expand the index passed in edx to
|
||||
// the whole rdx register. This is unnecessary and more expensive than just a mov,
|
||||
// which when done to a large register size automatically zeroes the upper bits.
|
||||
// With the (IntPtr)(void*)(uint) cast, we get the following codegen instead:
|
||||
// =============================
|
||||
// L0000: mov rax, [rcx]
|
||||
// L0003: mov edx, edx
|
||||
// L0005: lea rax, [rax+rdx*4]
|
||||
// L0009: ret
|
||||
// =============================
|
||||
// Here we can see how the index is extended to a native integer with just a mov,
|
||||
// which effectively only zeroes the upper bits of the same register used as source.
|
||||
// These three casts are a bit verbose, but they do the trick on both 32 bit and 64
|
||||
// bit architectures, producing optimal code in both cases (they are either completely
|
||||
// elided on 32 bit systems, or result in the correct register expansion when on 64 bit).
|
||||
// We first do an unchecked conversion to uint (which is just a reinterpret-cast). We
|
||||
// then cast to void*, which lets the following IntPtr cast avoid the range check on 32 bit
|
||||
// (since uint could be out of range there if the original index was negative). The final
|
||||
// result is a clean mov as shown above. This will eventually be natively supported by the
|
||||
// JIT compiler (see https://github.com/dotnet/runtime/issues/38794), but doing this here
|
||||
// still ensures the optimal codegen even on existing runtimes (eg. .NET Core 2.1 and 3.1).
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
ref T ri = ref Unsafe.Add(ref r0, i);
|
||||
ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i);
|
||||
|
||||
return ref ri;
|
||||
}
|
||||
|
@ -87,7 +117,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// </returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref readonly T DangerousGetLookupReferenceAt<T>(this ReadOnlySpan<T> span, int i)
|
||||
public static unsafe ref readonly T DangerousGetLookupReferenceAt<T>(this ReadOnlySpan<T> span, int i)
|
||||
{
|
||||
// Check whether the input is in range by first casting both
|
||||
// operands to uint and then comparing them, as this allows
|
||||
|
@ -99,19 +129,19 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
// The result is then negated, producing the value 0xFFFFFFFF
|
||||
// for valid indices, or 0 otherwise. The generated mask
|
||||
// is then combined with the original index. This leaves
|
||||
// the index intact if it was valid, otherwise zeroes it.
|
||||
// the index intact if it was valid, otherwise zeros it.
|
||||
// The computed offset is finally used to access the
|
||||
// lookup table, and it is guaranteed to never go out of
|
||||
// bounds unless the input span was just empty, which for a
|
||||
// lookup table can just be assumed to always be false.
|
||||
bool isInRange = (uint)i < (uint)span.Length;
|
||||
byte rangeFlag = Unsafe.As<bool, byte>(ref isInRange);
|
||||
int
|
||||
negativeFlag = rangeFlag - 1,
|
||||
uint
|
||||
negativeFlag = unchecked(rangeFlag - 1u),
|
||||
mask = ~negativeFlag,
|
||||
offset = i & mask;
|
||||
offset = (uint)i & mask;
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
ref T r1 = ref Unsafe.Add(ref r0, offset);
|
||||
ref T r1 = ref Unsafe.Add(ref r0, (IntPtr)(void*)offset);
|
||||
|
||||
return ref r1;
|
||||
}
|
||||
|
@ -164,12 +194,12 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <param name="value">The <typeparamref name="T"/> value to look for.</param>
|
||||
/// <returns>The number of occurrences of <paramref name="value"/> in <paramref name="span"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static int Count<T>(this ReadOnlySpan<T> span, T value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe int Count<T>(this ReadOnlySpan<T> span, T value)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
IntPtr length = (IntPtr)span.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)span.Length;
|
||||
|
||||
return SpanHelper.Count(ref r0, length, value);
|
||||
}
|
||||
|
@ -279,7 +309,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// even long sequences of values. For the reference implementation, see: <see href="http://www.cse.yorku.ca/~oz/hash.html"/>.
|
||||
/// For details on the used constants, see the details provided in this StackOverflow answer (as well as the accepted one):
|
||||
/// <see href="https://stackoverflow.com/questions/10696223/reason-for-5381-number-in-djb-hash-function/13809282#13809282"/>.
|
||||
/// Additionally, a comparison between some common hashing algoriths can be found in the reply to this StackExchange question:
|
||||
/// Additionally, a comparison between some common hashing algorithms can be found in the reply to this StackExchange question:
|
||||
/// <see href="https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed"/>.
|
||||
/// Note that the exact implementation is slightly different in this method when it is not called on a sequence of <see cref="byte"/>
|
||||
/// values: in this case the <see cref="object.GetHashCode"/> method will be invoked for each <typeparamref name="T"/> value in
|
||||
|
@ -291,11 +321,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>The Djb2 hash is fully deterministic and with no random components.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetDjb2HashCode<T>(this ReadOnlySpan<T> span)
|
||||
public static unsafe int GetDjb2HashCode<T>(this ReadOnlySpan<T> span)
|
||||
where T : notnull
|
||||
{
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
IntPtr length = (IntPtr)span.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)span.Length;
|
||||
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, length);
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the <paramref name="i"/> parameter is valid.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T DangerousGetReferenceAt<T>(this Span<T> span, int i)
|
||||
public static unsafe ref T DangerousGetReferenceAt<T>(this Span<T> span, int i)
|
||||
{
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
ref T ri = ref Unsafe.Add(ref r0, i);
|
||||
ref T ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i);
|
||||
|
||||
return ref ri;
|
||||
}
|
||||
|
@ -140,11 +140,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <returns>The number of occurrences of <paramref name="value"/> in <paramref name="span"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Count<T>(this Span<T> span, T value)
|
||||
public static unsafe int Count<T>(this Span<T> span, T value)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
IntPtr length = (IntPtr)span.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)span.Length;
|
||||
|
||||
return SpanHelper.Count(ref r0, length, value);
|
||||
}
|
||||
|
@ -211,11 +211,11 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>The Djb2 hash is fully deterministic and with no random components.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetDjb2HashCode<T>(this Span<T> span)
|
||||
public static unsafe int GetDjb2HashCode<T>(this Span<T> span)
|
||||
where T : notnull
|
||||
{
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
IntPtr length = (IntPtr)span.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)span.Length;
|
||||
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, length);
|
||||
}
|
||||
|
@ -223,7 +223,6 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the given reference is out of range.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static void ThrowArgumentOutOfRangeExceptionForInvalidReference()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("value", "The input reference does not belong to an element of the input span");
|
||||
|
|
|
@ -245,7 +245,6 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when <see cref="Read{T}"/> fails.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidOperationExceptionForEndOfStream()
|
||||
{
|
||||
throw new InvalidOperationException("The stream didn't contain enough data to read the requested item");
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if !NETCOREAPP3_1
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
using Microsoft.Toolkit.HighPerformance.Enumerables;
|
||||
using Microsoft.Toolkit.HighPerformance.Helpers.Internals;
|
||||
|
||||
|
@ -46,7 +48,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the <paramref name="i"/> parameter is valid.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref char DangerousGetReferenceAt(this string text, int i)
|
||||
public static unsafe ref char DangerousGetReferenceAt(this string text, int i)
|
||||
{
|
||||
#if NETCOREAPP3_1
|
||||
ref char r0 = ref Unsafe.AsRef(text.GetPinnableReference());
|
||||
|
@ -55,7 +57,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
#else
|
||||
ref char r0 = ref MemoryMarshal.GetReference(text.AsSpan());
|
||||
#endif
|
||||
ref char ri = ref Unsafe.Add(ref r0, i);
|
||||
ref char ri = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)i);
|
||||
|
||||
return ref ri;
|
||||
}
|
||||
|
@ -89,10 +91,10 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <returns>The number of occurrences of <paramref name="c"/> in <paramref name="text"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Count(this string text, char c)
|
||||
public static unsafe int Count(this string text, char c)
|
||||
{
|
||||
ref char r0 = ref text.DangerousGetReference();
|
||||
IntPtr length = (IntPtr)text.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)text.Length;
|
||||
|
||||
return SpanHelper.Count(ref r0, length, c);
|
||||
}
|
||||
|
@ -107,7 +109,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// {
|
||||
/// // Access the index and value of each item here...
|
||||
/// int index = item.Index;
|
||||
/// string value = item.Value;
|
||||
/// char value = item.Value;
|
||||
/// }
|
||||
/// </code>
|
||||
/// The compiler will take care of properly setting up the <see langword="foreach"/> loop with the type returned from this method.
|
||||
|
@ -155,10 +157,10 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
|
|||
/// <remarks>The Djb2 hash is fully deterministic and with no random components.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetDjb2HashCode(this string text)
|
||||
public static unsafe int GetDjb2HashCode(this string text)
|
||||
{
|
||||
ref char r0 = ref text.DangerousGetReference();
|
||||
IntPtr length = (IntPtr)text.Length;
|
||||
IntPtr length = (IntPtr)(void*)(uint)text.Length;
|
||||
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, length);
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// decrement the input parameter <paramref name="x"/> to ensure that the range of accepted
|
||||
/// values fits within the available 32 bits of the lookup table in use.
|
||||
/// For more info on this optimization technique, see <see href="https://egorbo.com/llvm-range-checks.html"/>.
|
||||
/// Here is how the code from the lik above would be implemented using this method:
|
||||
/// Here is how the code from the link above would be implemented using this method:
|
||||
/// <code>
|
||||
/// bool IsReservedCharacter(char c)
|
||||
/// {
|
||||
|
@ -103,6 +103,68 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given value has any bytes that are set to 0.
|
||||
/// That is, given a <see cref="uint"/> value, which has a total of 4 bytes,
|
||||
/// it checks whether any of those have all the bits set to 0.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to check.</param>
|
||||
/// <returns>Whether <paramref name="value"/> has any bytes set to 0.</returns>
|
||||
/// <remarks>
|
||||
/// This method contains no branches.
|
||||
/// For more background on this subject, see <see href="https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord"/>.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HasZeroByte(uint value)
|
||||
{
|
||||
return ((value - 0x0101_0101u) & ~value & 0x8080_8080u) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given value has any bytes that are set to 0.
|
||||
/// This method mirrors <see cref="HasZeroByte(uint)"/>, but with <see cref="ulong"/> values.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to check.</param>
|
||||
/// <returns>Whether <paramref name="value"/> has any bytes set to 0.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HasZeroByte(ulong value)
|
||||
{
|
||||
return ((value - 0x0101_0101_0101_0101ul) & ~value & 0x8080_8080_8080_8080ul) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a byte in the input <see cref="uint"/> value matches a target value.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to check.</param>
|
||||
/// <param name="target">The target byte to look for.</param>
|
||||
/// <returns>Whether <paramref name="value"/> has any bytes set to <paramref name="target"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method contains no branches.
|
||||
/// For more info, see <see href="https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord"/>.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HasByteEqualTo(uint value, byte target)
|
||||
{
|
||||
return HasZeroByte(value ^ (0x0101_0101u * target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a byte in the input <see cref="uint"/> value matches a target value.
|
||||
/// This method mirrors <see cref="HasByteEqualTo(uint,byte)"/>, but with <see cref="ulong"/> values.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to check.</param>
|
||||
/// <param name="target">The target byte to look for.</param>
|
||||
/// <returns>Whether <paramref name="value"/> has any bytes set to <paramref name="target"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HasByteEqualTo(ulong value, byte target)
|
||||
{
|
||||
return HasZeroByte(value ^ (0x0101_0101_0101_0101u * target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a bit to a specified value.
|
||||
/// </summary>
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// <remarks>The returned hash code is not processed through <see cref="HashCode"/> APIs.</remarks>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int CombineValues(ReadOnlySpan<T> span)
|
||||
internal static unsafe int CombineValues(ReadOnlySpan<T> span)
|
||||
{
|
||||
ref T r0 = ref MemoryMarshal.GetReference(span);
|
||||
|
||||
|
@ -67,13 +67,19 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
// compiler, so this branch will never actually be executed by the code.
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
{
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, (IntPtr)span.Length);
|
||||
return SpanHelper.GetDjb2HashCode(ref r0, (IntPtr)(void*)(uint)span.Length);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get the info for the target memory area to process
|
||||
// Get the info for the target memory area to process.
|
||||
// The line below is computing the total byte size for the span,
|
||||
// and we cast both input factors to uint first to avoid sign extensions
|
||||
// (they're both guaranteed to always be positive values), and to let the
|
||||
// JIT avoid the 64 bit computation entirely when running in a 32 bit
|
||||
// process. In that case it will just compute the byte size as a 32 bit
|
||||
// multiplication with overflow, which is guaranteed never to happen anyway.
|
||||
ref byte rb = ref Unsafe.As<T, byte>(ref r0);
|
||||
IntPtr length = (IntPtr)((long)span.Length * Unsafe.SizeOf<T>());
|
||||
IntPtr length = (IntPtr)(void*)((uint)span.Length * (uint)Unsafe.SizeOf<T>());
|
||||
|
||||
return SpanHelper.GetDjb2LikeByteHash(ref rb, length);
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals
|
|||
// and the final loop to combine the partial hash values.
|
||||
// Note that even when we use the vectorized path we don't need to do
|
||||
// any preprocessing to try to get memory aligned, as that would cause
|
||||
// the hashcodes to potentially be different for the same data.
|
||||
// the hash codes to potentially be different for the same data.
|
||||
if (Vector.IsHardwareAccelerated &&
|
||||
(byte*)length >= (byte*)(Vector<byte>.Count << 3))
|
||||
{
|
||||
|
|
|
@ -136,7 +136,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// </summary>
|
||||
/// <param name="i">The index of the batch to process</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Invoke(int i)
|
||||
public unsafe void Invoke(int i)
|
||||
{
|
||||
int
|
||||
low = i * this.batchSize,
|
||||
|
@ -147,7 +147,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
|
||||
for (int j = low; j < end; j++)
|
||||
{
|
||||
ref TItem rj = ref Unsafe.Add(ref r0, j);
|
||||
ref TItem rj = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)j);
|
||||
|
||||
Unsafe.AsRef(this.action).Invoke(rj);
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// </summary>
|
||||
/// <param name="i">The index of the batch to process</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Invoke(int i)
|
||||
public unsafe void Invoke(int i)
|
||||
{
|
||||
int
|
||||
low = i * this.batchSize,
|
||||
|
@ -147,7 +147,7 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
|
||||
for (int j = low; j < end; j++)
|
||||
{
|
||||
ref TItem rj = ref Unsafe.Add(ref r0, j);
|
||||
ref TItem rj = ref Unsafe.Add(ref r0, (IntPtr)(void*)(uint)j);
|
||||
|
||||
Unsafe.AsRef(this.action).Invoke(ref rj);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Helpers
|
||||
{
|
||||
|
@ -15,7 +14,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid parameter is specified for the minimum actions per thread.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread()
|
||||
{
|
||||
// Having the argument name here manually typed is
|
||||
|
@ -30,7 +28,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid start parameter is specified for 1D loops.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForStartGreaterThanEnd()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("start", "The start parameter must be less than or equal to end");
|
||||
|
@ -39,7 +36,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when a range has an index starting from an end.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForRangeIndexFromEnd(string name)
|
||||
{
|
||||
throw new ArgumentException("The bounds of the range can't start from an end", name);
|
||||
|
@ -48,7 +44,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid top parameter is specified for 2D loops.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForTopGreaterThanBottom()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("top", "The top parameter must be less than or equal to bottom");
|
||||
|
@ -57,7 +52,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
|
|||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid left parameter is specified for 2D loops.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForLeftGreaterThanRight()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("left", "The left parameter must be less than or equal to right");
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
- MemoryBufferWriter<T>: an IBufferWriter<T>: implementation that can wrap external Memory<T>: instances.
|
||||
- MemoryOwner<T>: an IMemoryOwner<T> implementation with an embedded length and a fast Span<T> accessor.
|
||||
- SpanOwner<T>: a stack-only type with the ability to rent a buffer of a specified length and getting a Span<T> from it.
|
||||
- StringPool: a configurable pool for string instances that be used to minimize allocations when creating multiple strings from char buffers.
|
||||
- String, array, Span<T>, Memory<T> extensions and more, all focused on high performance.
|
||||
- HashCode<T>: a SIMD-enabled extension of HashCode to quickly process sequences of values.
|
||||
- BitHelper: a class with helper methods to perform bit operations on numeric types.
|
||||
|
@ -78,9 +79,9 @@
|
|||
|
||||
<!-- NETCORE_RUNTIME: to avoid issues with APIs that assume a specific memory layout, we define a
|
||||
.NET Core runtime constant to indicate the either .NET Core 2.1 or .NET Core 3.1. These are
|
||||
runtimes with the same overall memory layout for objects (in particular: strings, SZ arrays
|
||||
runtimes with the same overall memory layout for objects (in particular: strings, SZ arrays,
|
||||
and 2D arrays). We can use this constant to make sure that APIs that are exclusively available
|
||||
for .NET Standard targets do not make any assumtpion of any internals of the runtime being
|
||||
for .NET Standard targets do not make any assumption of any internals of the runtime being
|
||||
actually used by the consumers. -->
|
||||
<DefineConstants>NETSTANDARD2_1_OR_GREATER;SPAN_RUNTIME_SUPPORT;NETCORE_RUNTIME</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -129,7 +129,6 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// <summary>
|
||||
/// Throws a <see cref="InvalidOperationException"/> when trying to access <see cref="Value"/> for a default instance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidOperationException()
|
||||
{
|
||||
throw new InvalidOperationException("The current instance doesn't have a value that can be accessed");
|
||||
|
|
|
@ -59,7 +59,7 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
get
|
||||
{
|
||||
// We know that the span will always have a length of either
|
||||
// 1 or 0, se instead of using a cmp instruction and setting the
|
||||
// 1 or 0, so instead of using a cmp instruction and setting the
|
||||
// zero flag to produce our boolean value, we can just cast
|
||||
// the length to byte without overflow checks (doing a cast will
|
||||
// also account for the byte endianness of the current system),
|
||||
|
@ -113,7 +113,6 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// <summary>
|
||||
/// Throws a <see cref="InvalidOperationException"/> when trying to access <see cref="Value"/> for a default instance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowInvalidOperationException()
|
||||
{
|
||||
throw new InvalidOperationException("The current instance doesn't have a value that can be accessed");
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// </summary>
|
||||
/// <param name="owner">The owner <see cref="object"/> to create a portable reference for.</param>
|
||||
/// <param name="offset">The target offset within <paramref name="owner"/> for the target reference.</param>
|
||||
/// <remarks>The <paramref name="offset"/> parameter is not validated, and it's responsability of the caller to ensure it's valid.</remarks>
|
||||
/// <remarks>The <paramref name="offset"/> parameter is not validated, and it's responsibility of the caller to ensure it's valid.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private ReadOnlyRef(object owner, IntPtr offset)
|
||||
{
|
||||
|
@ -83,7 +83,7 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// </summary>
|
||||
/// <param name="owner">The owner <see cref="object"/> to create a portable reference for.</param>
|
||||
/// <param name="value">The target reference to point to (it must be within <paramref name="owner"/>).</param>
|
||||
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsability of the caller to ensure it's valid.</remarks>
|
||||
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsibility of the caller to ensure it's valid.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlyRef(object owner, in T value)
|
||||
{
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace Microsoft.Toolkit.HighPerformance
|
|||
/// </summary>
|
||||
/// <param name="owner">The owner <see cref="object"/> to create a portable reference for.</param>
|
||||
/// <param name="value">The target reference to point to (it must be within <paramref name="owner"/>).</param>
|
||||
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsability of the caller to ensure it's valid.</remarks>
|
||||
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsibility of the caller to ensure it's valid.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Ref(object owner, ref T value)
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// 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;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
|
||||
|
@ -10,21 +11,24 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// <summary>
|
||||
/// A <see cref="Stream"/> implementation wrapping an <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
internal sealed class IMemoryOwnerStream : MemoryStream
|
||||
/// <typeparam name="TSource">The type of source to use for the underlying data.</typeparam>
|
||||
internal sealed class IMemoryOwnerStream<TSource> : MemoryStream<TSource>
|
||||
where TSource : struct, ISpanOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance currently in use.
|
||||
/// The <see cref="IDisposable"/> instance currently in use.
|
||||
/// </summary>
|
||||
private readonly IMemoryOwner<byte> memory;
|
||||
private readonly IDisposable disposable;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IMemoryOwnerStream"/> class.
|
||||
/// Initializes a new instance of the <see cref="IMemoryOwnerStream{TSource}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="memory">The input <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance to use.</param>
|
||||
public IMemoryOwnerStream(IMemoryOwner<byte> memory)
|
||||
: base(memory.Memory)
|
||||
/// <param name="source">The input <typeparamref name="TSource"/> instance to use.</param>
|
||||
/// <param name="disposable">The <see cref="IDisposable"/> instance currently in use.</param>
|
||||
public IMemoryOwnerStream(TSource source, IDisposable disposable)
|
||||
: base(source, false)
|
||||
{
|
||||
this.memory = memory;
|
||||
this.disposable = disposable;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -32,7 +36,7 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
this.memory.Dispose();
|
||||
this.disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +1,21 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// 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;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> implementation wrapping a <see cref="Memory{T}"/> or <see cref="ReadOnlyMemory{T}"/> instance.
|
||||
/// A factory class to produce <see cref="MemoryStream{TSource}"/> instances.
|
||||
/// </summary>
|
||||
internal partial class MemoryStream
|
||||
internal static partial class MemoryStream
|
||||
{
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when setting the <see cref="Stream.Position"/> property.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForPosition()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(Position), "The value for the property was not in the valid range.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> when an input buffer is <see langword="null"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentNullExceptionForBuffer()
|
||||
{
|
||||
throw new ArgumentNullException("buffer", "The buffer is null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForOffset()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("offset", "Offset can't be negative.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeExceptionForCount()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count", "Count can't be negative.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when the sum of offset and count exceeds the length of the target buffer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForLength()
|
||||
{
|
||||
throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws a <see cref="NotSupportedException"/> when trying to write on a readonly stream.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowNotSupportedExceptionForCanWrite()
|
||||
{
|
||||
throw new NotSupportedException("The current stream doesn't support writing.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target stream.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentExceptionForEndOfStreamOnWrite()
|
||||
public static void ThrowArgumentExceptionForEndOfStreamOnWrite()
|
||||
{
|
||||
throw new ArgumentException("The current stream can't contain the requested input data.");
|
||||
}
|
||||
|
@ -79,8 +23,7 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// <summary>
|
||||
/// Throws a <see cref="NotSupportedException"/> when trying to set the length of the stream.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowNotSupportedExceptionForSetLength()
|
||||
public static void ThrowNotSupportedExceptionForSetLength()
|
||||
{
|
||||
throw new NotSupportedException("Setting the length is not supported for this stream.");
|
||||
}
|
||||
|
@ -89,19 +32,65 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// Throws an <see cref="ArgumentException"/> when using an invalid seek mode.
|
||||
/// </summary>
|
||||
/// <returns>Nothing, as this method throws unconditionally.</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static long ThrowArgumentExceptionForSeekOrigin()
|
||||
public static long ThrowArgumentExceptionForSeekOrigin()
|
||||
{
|
||||
throw new ArgumentException("The input seek mode is not valid.", "origin");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when setting the <see cref="Stream.Position"/> property.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentOutOfRangeExceptionForPosition()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(Stream.Position), "The value for the property was not in the valid range.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> when an input buffer is <see langword="null"/>.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentNullExceptionForBuffer()
|
||||
{
|
||||
throw new ArgumentNullException("buffer", "The buffer is null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentOutOfRangeExceptionForOffset()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("offset", "Offset can't be negative.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentOutOfRangeExceptionForCount()
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("count", "Count can't be negative.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when the sum of offset and count exceeds the length of the target buffer.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentExceptionForLength()
|
||||
{
|
||||
throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws a <see cref="NotSupportedException"/> when trying to write on a readonly stream.
|
||||
/// </summary>
|
||||
private static void ThrowNotSupportedExceptionForCanWrite()
|
||||
{
|
||||
throw new NotSupportedException("The current stream doesn't support writing.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> when using a disposed <see cref="Stream"/> instance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowObjectDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(memory), "The current stream has already been disposed");
|
||||
throw new ObjectDisposedException("source", "The current stream has already been disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// 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.
|
||||
|
||||
|
@ -8,9 +8,9 @@ using System.Runtime.CompilerServices;
|
|||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> implementation wrapping a <see cref="System.Memory{T}"/> or <see cref="System.ReadOnlyMemory{T}"/> instance.
|
||||
/// A factory class to produce <see cref="MemoryStream{TSource}"/> instances.
|
||||
/// </summary>
|
||||
internal partial class MemoryStream
|
||||
internal static partial class MemoryStream
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the <see cref="Stream.Position"/> argument.
|
||||
|
@ -18,7 +18,7 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// <param name="position">The new <see cref="Stream.Position"/> value being set.</param>
|
||||
/// <param name="length">The maximum length of the target <see cref="Stream"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ValidatePosition(long position, int length)
|
||||
public static void ValidatePosition(long position, int length)
|
||||
{
|
||||
if ((ulong)position >= (ulong)length)
|
||||
{
|
||||
|
@ -33,7 +33,7 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// <param name="offset">The offset within <paramref name="buffer"/>.</param>
|
||||
/// <param name="count">The number of elements to process within <paramref name="buffer"/>.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ValidateBuffer(byte[]? buffer, int offset, int count)
|
||||
public static void ValidateBuffer(byte[]? buffer, int offset, int count)
|
||||
{
|
||||
if (buffer is null)
|
||||
{
|
||||
|
@ -57,24 +57,24 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the <see cref="CanWrite"/> property.
|
||||
/// Validates the <see cref="MemoryStream{TSource}.CanWrite"/> property.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ValidateCanWrite()
|
||||
public static void ValidateCanWrite(bool canWrite)
|
||||
{
|
||||
if (!CanWrite)
|
||||
if (!canWrite)
|
||||
{
|
||||
ThrowNotSupportedExceptionForCanWrite();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the current instance hasn't been disposed.
|
||||
/// Validates that a given <see cref="Stream"/> instance hasn't been disposed.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ValidateDisposed()
|
||||
public static void ValidateDisposed(bool disposed)
|
||||
{
|
||||
if (this.disposed)
|
||||
if (disposed)
|
||||
{
|
||||
ThrowObjectDisposedException();
|
||||
}
|
||||
|
|
|
@ -1,315 +1,95 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// 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;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> implementation wrapping a <see cref="Memory{T}"/> or <see cref="ReadOnlyMemory{T}"/> instance.
|
||||
/// A factory class to produce <see cref="MemoryStream{TSource}"/> instances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type is not marked as <see langword="sealed"/> so that it can be inherited by
|
||||
/// <see cref="IMemoryOwnerStream"/>, which adds the <see cref="IDisposable"/> support for
|
||||
/// the wrapped buffer. We're not worried about the performance penalty here caused by the JIT
|
||||
/// not being able to resolve the <see langword="callvirt"/> instruction, as this type is
|
||||
/// only exposed as a <see cref="Stream"/> anyway, so the generated code would be the same.
|
||||
/// </remarks>
|
||||
internal partial class MemoryStream : Stream
|
||||
internal static partial class MemoryStream
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether <see cref="memory"/> was actually a <see cref="ReadOnlyMemory{T}"/> instance.
|
||||
/// Creates a new <see cref="Stream"/> from the input <see cref="ReadOnlyMemory{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
private readonly bool isReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Memory{T}"/> instance currently in use.
|
||||
/// </summary>
|
||||
private Memory<byte> memory;
|
||||
|
||||
/// <summary>
|
||||
/// The current position within <see cref="memory"/>.
|
||||
/// </summary>
|
||||
private int position;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether or not the current instance has been disposed
|
||||
/// </summary>
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryStream"/> class.
|
||||
/// </summary>
|
||||
/// <param name="memory">The input <see cref="Memory{T}"/> instance to use.</param>
|
||||
public MemoryStream(Memory<byte> memory)
|
||||
/// <param name="memory">The input <see cref="ReadOnlyMemory{T}"/> instance.</param>
|
||||
/// <param name="isReadOnly">Indicates whether or not <paramref name="memory"/> can be written to.</param>
|
||||
/// <returns>A <see cref="Stream"/> wrapping the underlying data for <paramref name="memory"/>.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="memory"/> has an invalid data store.</exception>
|
||||
[Pure]
|
||||
public static Stream Create(ReadOnlyMemory<byte> memory, bool isReadOnly)
|
||||
{
|
||||
this.memory = memory;
|
||||
this.position = 0;
|
||||
this.isReadOnly = false;
|
||||
if (memory.IsEmpty)
|
||||
{
|
||||
// Return an empty stream if the memory was empty
|
||||
return new MemoryStream<ArrayOwner>(ArrayOwner.Empty, isReadOnly);
|
||||
}
|
||||
|
||||
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment))
|
||||
{
|
||||
var arraySpanSource = new ArrayOwner(segment.Array!, segment.Offset, segment.Count);
|
||||
|
||||
return new MemoryStream<ArrayOwner>(arraySpanSource, isReadOnly);
|
||||
}
|
||||
|
||||
if (MemoryMarshal.TryGetMemoryManager<byte, MemoryManager<byte>>(memory, out var memoryManager, out int start, out int length))
|
||||
{
|
||||
MemoryManagerOwner memoryManagerSpanSource = new MemoryManagerOwner(memoryManager, start, length);
|
||||
|
||||
return new MemoryStream<MemoryManagerOwner>(memoryManagerSpanSource, isReadOnly);
|
||||
}
|
||||
|
||||
return ThrowNotSupportedExceptionForInvalidMemory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryStream"/> class.
|
||||
/// Creates a new <see cref="Stream"/> from the input <see cref="IMemoryOwner{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="memory">The input <see cref="ReadOnlyMemory{T}"/> instance to use.</param>
|
||||
public MemoryStream(ReadOnlyMemory<byte> memory)
|
||||
/// <param name="memoryOwner">The input <see cref="IMemoryOwner{T}"/> instance.</param>
|
||||
/// <returns>A <see cref="Stream"/> wrapping the underlying data for <paramref name="memoryOwner"/>.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="memoryOwner"/> has an invalid data store.</exception>
|
||||
[Pure]
|
||||
public static Stream Create(IMemoryOwner<byte> memoryOwner)
|
||||
{
|
||||
this.memory = MemoryMarshal.AsMemory(memory);
|
||||
this.position = 0;
|
||||
this.isReadOnly = true;
|
||||
Memory<byte> memory = memoryOwner.Memory;
|
||||
|
||||
if (memory.IsEmpty)
|
||||
{
|
||||
// Return an empty stream if the memory was empty
|
||||
return new IMemoryOwnerStream<ArrayOwner>(ArrayOwner.Empty, memoryOwner);
|
||||
}
|
||||
|
||||
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment))
|
||||
{
|
||||
var arraySpanSource = new ArrayOwner(segment.Array!, segment.Offset, segment.Count);
|
||||
|
||||
return new IMemoryOwnerStream<ArrayOwner>(arraySpanSource, memoryOwner);
|
||||
}
|
||||
|
||||
if (MemoryMarshal.TryGetMemoryManager<byte, MemoryManager<byte>>(memory, out var memoryManager, out int start, out int length))
|
||||
{
|
||||
MemoryManagerOwner memoryManagerSpanSource = new MemoryManagerOwner(memoryManager, start, length);
|
||||
|
||||
return new IMemoryOwnerStream<MemoryManagerOwner>(memoryManagerSpanSource, memoryOwner);
|
||||
}
|
||||
|
||||
return ThrowNotSupportedExceptionForInvalidMemory();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override bool CanRead
|
||||
/// <summary>
|
||||
/// Throws a <see cref="ArgumentException"/> when a given <see cref="Memory{T}"/>
|
||||
/// or <see cref="IMemoryOwner{T}"/> instance has an unsupported backing store.
|
||||
/// </summary>
|
||||
/// <returns>Nothing, this method always throws.</returns>
|
||||
private static Stream ThrowNotSupportedExceptionForInvalidMemory()
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => !this.disposed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override bool CanSeek
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => !this.disposed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override bool CanWrite
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => !this.isReadOnly && !this.disposed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override long Length
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
ValidateDisposed();
|
||||
|
||||
return this.memory.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override long Position
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
ValidateDisposed();
|
||||
|
||||
return this.position;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set
|
||||
{
|
||||
ValidateDisposed();
|
||||
ValidatePosition(value, this.memory.Length);
|
||||
|
||||
this.position = unchecked((int)value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CopyTo(destination, bufferSize);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
return Task.FromCanceled(e.CancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task<int> ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled<int>(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int result = Read(buffer, offset, count);
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
return Task.FromCanceled<int>(e.CancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException<int>(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Write(buffer, offset, count);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
return Task.FromCanceled(e.CancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
ValidateDisposed();
|
||||
|
||||
long index = origin switch
|
||||
{
|
||||
SeekOrigin.Begin => offset,
|
||||
SeekOrigin.Current => this.position + offset,
|
||||
SeekOrigin.End => this.memory.Length + offset,
|
||||
_ => ThrowArgumentExceptionForSeekOrigin()
|
||||
};
|
||||
|
||||
ValidatePosition(index, this.memory.Length);
|
||||
|
||||
this.position = unchecked((int)index);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void SetLength(long value)
|
||||
{
|
||||
ThrowNotSupportedExceptionForSetLength();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override int Read(byte[]? buffer, int offset, int count)
|
||||
{
|
||||
ValidateDisposed();
|
||||
ValidateBuffer(buffer, offset, count);
|
||||
|
||||
int
|
||||
bytesAvailable = this.memory.Length - this.position,
|
||||
bytesCopied = Math.Min(bytesAvailable, count);
|
||||
|
||||
Span<byte>
|
||||
source = this.memory.Span.Slice(this.position, bytesCopied),
|
||||
destination = buffer.AsSpan(offset, bytesCopied);
|
||||
|
||||
source.CopyTo(destination);
|
||||
|
||||
this.position += bytesCopied;
|
||||
|
||||
return bytesCopied;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override int ReadByte()
|
||||
{
|
||||
ValidateDisposed();
|
||||
|
||||
if (this.position == this.memory.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.memory.Span[this.position++];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void Write(byte[]? buffer, int offset, int count)
|
||||
{
|
||||
ValidateDisposed();
|
||||
ValidateCanWrite();
|
||||
ValidateBuffer(buffer, offset, count);
|
||||
|
||||
Span<byte>
|
||||
source = buffer.AsSpan(offset, count),
|
||||
destination = this.memory.Span.Slice(this.position);
|
||||
|
||||
if (!source.TryCopyTo(destination))
|
||||
{
|
||||
ThrowArgumentExceptionForEndOfStreamOnWrite();
|
||||
}
|
||||
|
||||
this.position += source.Length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void WriteByte(byte value)
|
||||
{
|
||||
ValidateDisposed();
|
||||
ValidateCanWrite();
|
||||
|
||||
if (this.position == this.memory.Length)
|
||||
{
|
||||
ThrowArgumentExceptionForEndOfStreamOnWrite();
|
||||
}
|
||||
|
||||
this.memory.Span[this.position++] = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (this.disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposed = true;
|
||||
this.memory = default;
|
||||
throw new ArgumentException("The input instance doesn't have a valid underlying data store.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,17 +11,15 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> implementation wrapping a <see cref="Memory{T}"/> or <see cref="ReadOnlyMemory{T}"/> instance.
|
||||
/// </summary>
|
||||
internal partial class MemoryStream
|
||||
/// <inheritdoc cref="MemoryStream{TSource}"/>
|
||||
internal partial class MemoryStream<TSource>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyTo(Stream destination, int bufferSize)
|
||||
{
|
||||
ValidateDisposed();
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
|
||||
Span<byte> source = this.memory.Span.Slice(this.position);
|
||||
Span<byte> source = this.source.Span.Slice(this.position);
|
||||
|
||||
this.position += source.Length;
|
||||
|
||||
|
@ -79,13 +77,13 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// <inheritdoc/>
|
||||
public sealed override int Read(Span<byte> buffer)
|
||||
{
|
||||
ValidateDisposed();
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
|
||||
int
|
||||
bytesAvailable = this.memory.Length - this.position,
|
||||
bytesAvailable = this.source.Length - this.position,
|
||||
bytesCopied = Math.Min(bytesAvailable, buffer.Length);
|
||||
|
||||
Span<byte> source = this.memory.Span.Slice(this.position, bytesCopied);
|
||||
Span<byte> source = this.source.Span.Slice(this.position, bytesCopied);
|
||||
|
||||
source.CopyTo(buffer);
|
||||
|
||||
|
@ -97,14 +95,14 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
|
|||
/// <inheritdoc/>
|
||||
public sealed override void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
ValidateDisposed();
|
||||
ValidateCanWrite();
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
MemoryStream.ValidateCanWrite(CanWrite);
|
||||
|
||||
Span<byte> destination = this.memory.Span.Slice(this.position);
|
||||
Span<byte> destination = this.source.Span.Slice(this.position);
|
||||
|
||||
if (!buffer.TryCopyTo(destination))
|
||||
{
|
||||
ThrowArgumentExceptionForEndOfStreamOnWrite();
|
||||
MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite();
|
||||
}
|
||||
|
||||
this.position += buffer.Length;
|
|
@ -0,0 +1,305 @@
|
|||
// 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;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> implementation wrapping a <see cref="Memory{T}"/> or <see cref="ReadOnlyMemory{T}"/> instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of source to use for the underlying data.</typeparam>
|
||||
/// <remarks>
|
||||
/// This type is not marked as <see langword="sealed"/> so that it can be inherited by
|
||||
/// <see cref="IMemoryOwnerStream{TSource}"/>, which adds the <see cref="IDisposable"/> support for
|
||||
/// the wrapped buffer. We're not worried about the performance penalty here caused by the JIT
|
||||
/// not being able to resolve the <see langword="callvirt"/> instruction, as this type is
|
||||
/// only exposed as a <see cref="Stream"/> anyway, so the generated code would be the same.
|
||||
/// </remarks>
|
||||
internal partial class MemoryStream<TSource> : Stream
|
||||
where TSource : struct, ISpanOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether <see cref="source"/> can be written to.
|
||||
/// </summary>
|
||||
private readonly bool isReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// The <typeparamref name="TSource"/> instance currently in use.
|
||||
/// </summary>
|
||||
private TSource source;
|
||||
|
||||
/// <summary>
|
||||
/// The current position within <see cref="source"/>.
|
||||
/// </summary>
|
||||
private int position;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether or not the current instance has been disposed
|
||||
/// </summary>
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryStream{TSource}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="source">The input <typeparamref name="TSource"/> instance to use.</param>
|
||||
/// <param name="isReadOnly">Indicates whether <paramref name="source"/> can be written to.</param>
|
||||
public MemoryStream(TSource source, bool isReadOnly)
|
||||
{
|
||||
this.source = source;
|
||||
this.isReadOnly = isReadOnly;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override bool CanRead
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => !this.disposed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override bool CanSeek
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => !this.disposed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override bool CanWrite
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => !this.isReadOnly && !this.disposed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override long Length
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
|
||||
return this.source.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override long Position
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
|
||||
return this.position;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
MemoryStream.ValidatePosition(value, this.source.Length);
|
||||
|
||||
this.position = unchecked((int)value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CopyTo(destination, bufferSize);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
return Task.FromCanceled(e.CancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task<int> ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled<int>(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int result = Read(buffer, offset, count);
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
return Task.FromCanceled<int>(e.CancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException<int>(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Write(buffer, offset, count);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
return Task.FromCanceled(e.CancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
|
||||
long index = origin switch
|
||||
{
|
||||
SeekOrigin.Begin => offset,
|
||||
SeekOrigin.Current => this.position + offset,
|
||||
SeekOrigin.End => this.source.Length + offset,
|
||||
_ => MemoryStream.ThrowArgumentExceptionForSeekOrigin()
|
||||
};
|
||||
|
||||
MemoryStream.ValidatePosition(index, this.source.Length);
|
||||
|
||||
this.position = unchecked((int)index);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void SetLength(long value)
|
||||
{
|
||||
MemoryStream.ThrowNotSupportedExceptionForSetLength();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override int Read(byte[]? buffer, int offset, int count)
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
MemoryStream.ValidateBuffer(buffer, offset, count);
|
||||
|
||||
int
|
||||
bytesAvailable = this.source.Length - this.position,
|
||||
bytesCopied = Math.Min(bytesAvailable, count);
|
||||
|
||||
Span<byte>
|
||||
source = this.source.Span.Slice(this.position, bytesCopied),
|
||||
destination = buffer.AsSpan(offset, bytesCopied);
|
||||
|
||||
source.CopyTo(destination);
|
||||
|
||||
this.position += bytesCopied;
|
||||
|
||||
return bytesCopied;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override int ReadByte()
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
|
||||
if (this.position == this.source.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.source.Span[this.position++];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void Write(byte[]? buffer, int offset, int count)
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
MemoryStream.ValidateCanWrite(CanWrite);
|
||||
MemoryStream.ValidateBuffer(buffer, offset, count);
|
||||
|
||||
Span<byte>
|
||||
source = buffer.AsSpan(offset, count),
|
||||
destination = this.source.Span.Slice(this.position);
|
||||
|
||||
if (!source.TryCopyTo(destination))
|
||||
{
|
||||
MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite();
|
||||
}
|
||||
|
||||
this.position += source.Length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void WriteByte(byte value)
|
||||
{
|
||||
MemoryStream.ValidateDisposed(this.disposed);
|
||||
MemoryStream.ValidateCanWrite(CanWrite);
|
||||
|
||||
if (this.position == this.source.Length)
|
||||
{
|
||||
MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite();
|
||||
}
|
||||
|
||||
this.source.Span[this.position++] = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (this.disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposed = true;
|
||||
this.source = default;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// 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;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Toolkit.HighPerformance.Extensions;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISpanOwner"/> implementation wrapping an array.
|
||||
/// </summary>
|
||||
internal readonly struct ArrayOwner : ISpanOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The wrapped <see cref="byte"/> array.
|
||||
/// </summary>
|
||||
private readonly byte[] array;
|
||||
|
||||
/// <summary>
|
||||
/// The starting offset within <see cref="array"/>.
|
||||
/// </summary>
|
||||
private readonly int offset;
|
||||
|
||||
/// <summary>
|
||||
/// The usable length within <see cref="array"/>.
|
||||
/// </summary>
|
||||
private readonly int length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArrayOwner"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="array">The wrapped <see cref="byte"/> array.</param>
|
||||
/// <param name="offset">The starting offset within <paramref name="array"/>.</param>
|
||||
/// <param name="length">The usable length within <paramref name="array"/>.</param>
|
||||
public ArrayOwner(byte[] array, int offset, int length)
|
||||
{
|
||||
this.array = array;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty <see cref="ArrayOwner"/> instance.
|
||||
/// </summary>
|
||||
public static ArrayOwner Empty
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new ArrayOwner(Array.Empty<byte>(), 0, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Length
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Span<byte> Span
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
#if SPAN_RUNTIME_SUPPORT
|
||||
ref byte r0 = ref this.array.DangerousGetReferenceAt(this.offset);
|
||||
|
||||
return MemoryMarshal.CreateSpan(ref r0, this.length);
|
||||
#else
|
||||
return this.array.AsSpan(this.offset, this.length);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for types acting as sources for <see cref="Span{T}"/> instances.
|
||||
/// </summary>
|
||||
internal interface ISpanOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the length of the underlying memory area.
|
||||
/// </summary>
|
||||
int Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Span{T}"/> instance wrapping the underlying memory area.
|
||||
/// </summary>
|
||||
Span<byte> Span { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// 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;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.HighPerformance.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISpanOwner"/> implementation wrapping a <see cref="MemoryManager{T}"/> of <see cref="byte"/> instance.
|
||||
/// </summary>
|
||||
internal readonly struct MemoryManagerOwner : ISpanOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The wrapped <see cref="MemoryManager{T}"/> instance.
|
||||
/// </summary>
|
||||
private readonly MemoryManager<byte> memoryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The starting offset within <see cref="memoryManager"/>.
|
||||
/// </summary>
|
||||
private readonly int offset;
|
||||
|
||||
/// <summary>
|
||||
/// The usable length within <see cref="memoryManager"/>.
|
||||
/// </summary>
|
||||
private readonly int length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryManagerOwner"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="memoryManager">The wrapped <see cref="MemoryManager{T}"/> instance.</param>
|
||||
/// <param name="offset">The starting offset within <paramref name="memoryManager"/>.</param>
|
||||
/// <param name="length">The usable length within <paramref name="memoryManager"/>.</param>
|
||||
public MemoryManagerOwner(MemoryManager<byte> memoryManager, int offset, int length)
|
||||
{
|
||||
this.memoryManager = memoryManager;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Length
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.length;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Span<byte> Span
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
// We can't use the same trick we use for arrays to optimize the creation of
|
||||
// the offset span, as otherwise a bugged MemoryManager<T> instance returning
|
||||
// a span of an incorrect size could cause an access violation. Instead, we just
|
||||
// get the span and then slice it, which will validate both offset and length.
|
||||
return this.memoryManager.GetSpan().Slice(this.offset, this.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,567 @@
|
|||
// 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.
|
||||
|
||||
#pragma warning disable SA1512
|
||||
|
||||
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.ComponentModel
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for objects of which the properties must be observable.
|
||||
/// </summary>
|
||||
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
|
||||
{
|
||||
/// <inheritdoc cref="INotifyPropertyChanged.PropertyChanged"/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc cref="INotifyPropertyChanging.PropertyChanging"/>
|
||||
public event PropertyChangingEventHandler? PropertyChanging;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the required configuration when a property has changed, and then
|
||||
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <remarks>The base implementation only raises the <see cref="PropertyChanged"/> event.</remarks>
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the required configuration when a property is changing, and then
|
||||
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <remarks>The base implementation only raises the <see cref="PropertyChanging"/> event.</remarks>
|
||||
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
|
||||
/// value, then raises the <see cref="PropertyChanged"/> event.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
|
||||
/// if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(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<T> will be inlined, and we need the JIT
|
||||
// to be able to see the full EqualityComparer<T>.Default.Equals call, so that
|
||||
// it'll use the intrinsics version of it and just replace the whole invocation
|
||||
// with a direct comparison when possible (eg. for primitive numeric types).
|
||||
// This is the fastest SetProperty<T> overload so we particularly care about
|
||||
// the codegen quality here, and the code is small and simple enough so that
|
||||
// duplicating it still doesn't make the whole class harder to maintain.
|
||||
if (EqualityComparer<T>.Default.Equals(field, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
field = newValue;
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
|
||||
/// value, then raises the <see cref="PropertyChanged"/> event.
|
||||
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (comparer.Equals(field, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
field = newValue;
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
|
||||
/// value, then raises the <see cref="PropertyChanged"/> event.
|
||||
/// This overload is much less efficient than <see cref="SetProperty{T}(ref T,T,string)"/> and it
|
||||
/// should only be used when the former is not viable (eg. when the target property being
|
||||
/// updated does not directly expose a backing field that can be passed by reference).
|
||||
/// For performance reasons, it is recommended to use a stateful callback if possible through
|
||||
/// the <see cref="SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string?)"/> whenever possible
|
||||
/// instead of this overload, as that will allow the C# compiler to cache the input callback and
|
||||
/// reduce the memory allocations. More info on that overload are available in the related XML
|
||||
/// docs. This overload is here for completeness and in cases where that is not applicable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
|
||||
/// if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
// We avoid calling the overload again to ensure the comparison is inlined
|
||||
if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
callback(newValue);
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
|
||||
/// value, then raises the <see cref="PropertyChanged"/> event.
|
||||
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (comparer.Equals(oldValue, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
callback(newValue);
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given nested property. If the value has changed,
|
||||
/// raises the <see cref="PropertyChanging"/> event, updates the property and then raises the
|
||||
/// <see cref="PropertyChanged"/> event. The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>,
|
||||
/// with the difference being that this method is used to relay properties from a wrapped model in the
|
||||
/// current instance. This type is useful when creating wrapping, bindable objects that operate over
|
||||
/// models that lack support for notification (eg. for CRUD operations).
|
||||
/// Suppose we have this model (eg. for a database row in a table):
|
||||
/// <code>
|
||||
/// public class Person
|
||||
/// {
|
||||
/// public string Name { get; set; }
|
||||
/// }
|
||||
/// </code>
|
||||
/// We can then use a property to wrap instances of this type into our observable model (which supports
|
||||
/// notifications), injecting the notification to the properties of that model, like so:
|
||||
/// <code>
|
||||
/// public class BindablePerson : ObservableObject
|
||||
/// {
|
||||
/// public Model { get; }
|
||||
///
|
||||
/// public BindablePerson(Person model)
|
||||
/// {
|
||||
/// Model = model;
|
||||
/// }
|
||||
///
|
||||
/// public string Name
|
||||
/// {
|
||||
/// get => Model.Name;
|
||||
/// set => Set(Model.Name, value, Model, (model, name) => model.Name = name);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// This way we can then use the wrapping object in our application, and all those "proxy" properties will
|
||||
/// also raise notifications when changed. Note that this method is not meant to be a replacement for
|
||||
/// <see cref="SetProperty{T}(ref T,T,string)"/>, and it should only be used when relaying properties to a model that
|
||||
/// doesn't support notifications, and only if you can't implement notifications to that model directly (eg. by having
|
||||
/// it inherit from <see cref="ObservableObject"/>). The syntax relies on passing the target model and a stateless callback
|
||||
/// to allow the C# compiler to cache the function, which results in much better performance and no memory usage.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="model">The model containing the property being updated.</param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not
|
||||
/// raised if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
callback(model, newValue);
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given nested property. If the value has changed,
|
||||
/// raises the <see cref="PropertyChanging"/> event, updates the property and then raises the
|
||||
/// <see cref="PropertyChanged"/> event. The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>,
|
||||
/// with the difference being that this method is used to relay properties from a wrapped model in the
|
||||
/// current instance. See additional notes about this overload in <see cref="SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="model">The model containing the property being updated.</param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
if (comparer.Equals(oldValue, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
callback(model, newValue);
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given field (which should be the backing
|
||||
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
|
||||
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
|
||||
/// The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>, with the difference being that
|
||||
/// this method will also monitor the new value of the property (a generic <see cref="Task"/>) and will also
|
||||
/// raise the <see cref="PropertyChanged"/> again for the target property when it completes.
|
||||
/// This can be used to update bindings observing that <see cref="Task"/> or any of its properties.
|
||||
/// This method and its overload specifically rely on the <see cref="TaskNotifier"/> type, which needs
|
||||
/// to be used in the backing field for the target <see cref="Task"/> property. The field doesn't need to be
|
||||
/// initialized, as this method will take care of doing that automatically. The <see cref="TaskNotifier"/>
|
||||
/// type also includes an implicit operator, so it can be assigned to any <see cref="Task"/> instance directly.
|
||||
/// Here is a sample property declaration using this method:
|
||||
/// <code>
|
||||
/// private TaskNotifier myTask;
|
||||
///
|
||||
/// public Task MyTask
|
||||
/// {
|
||||
/// get => myTask;
|
||||
/// private set => SetAndNotifyOnCompletion(ref myTask, value);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="taskNotifier">The field notifier to modify.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised if the current
|
||||
/// and new value for the target property are the same. The return value being <see langword="true"/> only
|
||||
/// indicates that the new value being assigned to <paramref name="taskNotifier"/> is different than the previous one,
|
||||
/// and it does not mean the new <see cref="Task"/> instance passed as argument is in any particular state.
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion(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
|
||||
// static singleton field containing a closure instance, and another caching the instantiated Action<TTask>
|
||||
// instance. This will result in no further allocations after the first time this method is called for a given
|
||||
// generic type. We only pay the cost of the virtual call to the delegate, but this is not performance critical
|
||||
// code and that overhead would still be much lower than the rest of the method anyway, so that's fine.
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, _ => { }, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given field (which should be the backing
|
||||
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
|
||||
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
|
||||
/// This method is just like <see cref="SetPropertyAndNotifyOnCompletion(ref TaskNotifier,Task,string)"/>,
|
||||
/// with the difference being an extra <see cref="Action{T}"/> parameter with a callback being invoked
|
||||
/// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
|
||||
/// </summary>
|
||||
/// <param name="taskNotifier">The field notifier to modify.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
|
||||
/// if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action<Task?> callback, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given field (which should be the backing
|
||||
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
|
||||
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
|
||||
/// The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>, with the difference being that
|
||||
/// this method will also monitor the new value of the property (a generic <see cref="Task"/>) and will also
|
||||
/// raise the <see cref="PropertyChanged"/> again for the target property when it completes.
|
||||
/// This can be used to update bindings observing that <see cref="Task"/> or any of its properties.
|
||||
/// This method and its overload specifically rely on the <see cref="TaskNotifier{T}"/> type, which needs
|
||||
/// to be used in the backing field for the target <see cref="Task"/> property. The field doesn't need to be
|
||||
/// initialized, as this method will take care of doing that automatically. The <see cref="TaskNotifier{T}"/>
|
||||
/// type also includes an implicit operator, so it can be assigned to any <see cref="Task"/> instance directly.
|
||||
/// Here is a sample property declaration using this method:
|
||||
/// <code>
|
||||
/// private TaskNotifier<int> myTask;
|
||||
///
|
||||
/// public Task<int> MyTask
|
||||
/// {
|
||||
/// get => myTask;
|
||||
/// private set => SetAndNotifyOnCompletion(ref myTask, value);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of result for the <see cref="Task{TResult}"/> to set and monitor.</typeparam>
|
||||
/// <param name="taskNotifier">The field notifier to modify.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised if the current
|
||||
/// and new value for the target property are the same. The return value being <see langword="true"/> only
|
||||
/// indicates that the new value being assigned to <paramref name="taskNotifier"/> is different than the previous one,
|
||||
/// and it does not mean the new <see cref="Task{TResult}"/> instance passed as argument is in any particular state.
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>? taskNotifier, Task<T>? newValue, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, _ => { }, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given field (which should be the backing
|
||||
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
|
||||
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
|
||||
/// This method is just like <see cref="SetPropertyAndNotifyOnCompletion{T}(ref TaskNotifier{T},Task{T},string)"/>,
|
||||
/// with the difference being an extra <see cref="Action{T}"/> parameter with a callback being invoked
|
||||
/// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of result for the <see cref="Task{TResult}"/> to set and monitor.</typeparam>
|
||||
/// <param name="taskNotifier">The field notifier to modify.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
|
||||
/// if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>? taskNotifier, Task<T>? newValue, Action<Task<T>?> callback, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the notification logic for the related methods.
|
||||
/// </summary>
|
||||
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
|
||||
/// <param name="taskNotifier">The field notifier.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?> callback, [CallerMemberName] string? propertyName = null)
|
||||
where TTask : Task
|
||||
{
|
||||
if (ReferenceEquals(taskNotifier.Task, newValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the status of the new task before assigning it to the
|
||||
// target field. This is so that in case the task is either
|
||||
// null or already completed, we can avoid the overhead of
|
||||
// scheduling the method to monitor its completion.
|
||||
bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true;
|
||||
|
||||
OnPropertyChanging(propertyName);
|
||||
|
||||
taskNotifier.Task = newValue;
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
// If the input task is either null or already completed, we don't need to
|
||||
// execute the additional logic to monitor its completion, so we can just bypass
|
||||
// the rest of the method and return that the field changed here. The return value
|
||||
// does not indicate that the task itself has completed, but just that the property
|
||||
// value itself has changed (ie. the referenced task instance has changed).
|
||||
// This mirrors the return value of all the other synchronous Set methods as well.
|
||||
if (isAlreadyCompletedOrNull)
|
||||
{
|
||||
callback(newValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// We use a local async function here so that the main method can
|
||||
// remain synchronous and return a value that can be immediately
|
||||
// used by the caller. This mirrors Set<T>(ref T, T, string).
|
||||
// We use an async void function instead of a Task-returning function
|
||||
// so that if a binding update caused by the property change notification
|
||||
// causes a crash, it is immediately reported in the application instead of
|
||||
// the exception being ignored (as the returned task wouldn't be awaited),
|
||||
// which would result in a confusing behavior for users.
|
||||
async void MonitorTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Await the task and ignore any exceptions
|
||||
await newValue!;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// Only notify if the property hasn't changed
|
||||
if (ReferenceEquals(taskNotifier.Task, newValue))
|
||||
{
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
callback(newValue);
|
||||
}
|
||||
|
||||
MonitorTask();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An interface for task notifiers of a specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TTask">The type of value to store.</typeparam>
|
||||
private interface ITaskNotifier<TTask>
|
||||
where TTask : Task
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the wrapped <typeparamref name="TTask"/> value.
|
||||
/// </summary>
|
||||
TTask? Task { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A wrapping class that can hold a <see cref="Task"/> value.
|
||||
/// </summary>
|
||||
protected sealed class TaskNotifier : ITaskNotifier<Task>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TaskNotifier"/> class.
|
||||
/// </summary>
|
||||
internal TaskNotifier()
|
||||
{
|
||||
}
|
||||
|
||||
private Task? task;
|
||||
|
||||
/// <inheritdoc/>
|
||||
Task? ITaskNotifier<Task>.Task
|
||||
{
|
||||
get => this.task;
|
||||
set => this.task = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unwraps the <see cref="Task"/> value stored in the current instance.
|
||||
/// </summary>
|
||||
/// <param name="notifier">The input <see cref="TaskNotifier{TTask}"/> instance.</param>
|
||||
public static implicit operator Task?(TaskNotifier? notifier)
|
||||
{
|
||||
return notifier?.task;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A wrapping class that can hold a <see cref="Task{T}"/> value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value for the wrapped <see cref="Task{T}"/> instance.</typeparam>
|
||||
protected sealed class TaskNotifier<T> : ITaskNotifier<Task<T>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TaskNotifier{TTask}"/> class.
|
||||
/// </summary>
|
||||
internal TaskNotifier()
|
||||
{
|
||||
}
|
||||
|
||||
private Task<T>? task;
|
||||
|
||||
/// <inheritdoc/>
|
||||
Task<T>? ITaskNotifier<Task<T>>.Task
|
||||
{
|
||||
get => this.task;
|
||||
set => this.task = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unwraps the <see cref="Task{T}"/> value stored in the current instance.
|
||||
/// </summary>
|
||||
/// <param name="notifier">The input <see cref="TaskNotifier{TTask}"/> instance.</param>
|
||||
public static implicit operator Task<T>?(TaskNotifier<T>? notifier)
|
||||
{
|
||||
return notifier?.task;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,304 @@
|
|||
// 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.
|
||||
|
||||
#pragma warning disable SA1512
|
||||
|
||||
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Toolkit.Mvvm.Messaging;
|
||||
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.ComponentModel
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for observable objects that also acts as recipients for messages. This class is an extension of
|
||||
/// <see cref="ObservableObject"/> which also provides built-in support to use the <see cref="IMessenger"/> type.
|
||||
/// </summary>
|
||||
public abstract class ObservableRecipient : ObservableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This constructor will produce an instance that will use the <see cref="WeakReferenceMessenger.Default"/> instance
|
||||
/// to perform requested operations. It will also be available locally through the <see cref="Messenger"/> property.
|
||||
/// </remarks>
|
||||
protected ObservableRecipient()
|
||||
: this(WeakReferenceMessenger.Default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send messages.</param>
|
||||
protected ObservableRecipient(IMessenger messenger)
|
||||
{
|
||||
Messenger = messenger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IMessenger"/> instance in use.
|
||||
/// </summary>
|
||||
protected IMessenger Messenger { get; }
|
||||
|
||||
private bool isActive;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the current view model is currently active.
|
||||
/// </summary>
|
||||
public bool IsActive
|
||||
{
|
||||
get => this.isActive;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref this.isActive, value, true))
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
OnActivated();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDeactivated();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
|
||||
/// Use this method to register to messages and do other initialization for this instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base implementation registers all messages for this recipients that have been declared
|
||||
/// explicitly through the <see cref="IRecipient{TMessage}"/> interface, using the default channel.
|
||||
/// For more details on how this works, see the <see cref="IMessengerExtensions.RegisterAll"/> method.
|
||||
/// If you need more fine tuned control, want to register messages individually or just prefer
|
||||
/// the lambda-style syntax for message registration, override this method and register manually.
|
||||
/// </remarks>
|
||||
protected virtual void OnActivated()
|
||||
{
|
||||
Messenger.RegisterAll(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
|
||||
/// Use this method to unregister from messages and do general cleanup for this instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base implementation unregisters all messages for this recipient. It does so by
|
||||
/// invoking <see cref="IMessenger.UnregisterAll"/>, which removes all registered
|
||||
/// handlers for a given subscriber, regardless of what token was used to register them.
|
||||
/// That is, all registered handlers across all subscription channels will be removed.
|
||||
/// </remarks>
|
||||
protected virtual void OnDeactivated()
|
||||
{
|
||||
Messenger.UnregisterAll(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts a <see cref="PropertyChangedMessage{T}"/> with the specified
|
||||
/// parameters, without using any particular token (so using the default channel).
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The value of the property before it changed.</param>
|
||||
/// <param name="newValue">The value of the property after it changed.</param>
|
||||
/// <param name="propertyName">The name of the property that changed.</param>
|
||||
/// <remarks>
|
||||
/// You should override this method if you wish to customize the channel being
|
||||
/// used to send the message (eg. if you need to use a specific token for the channel).
|
||||
/// </remarks>
|
||||
protected virtual void Broadcast<T>(T oldValue, T newValue, string? propertyName)
|
||||
{
|
||||
var message = new PropertyChangedMessage<T>(this, propertyName, oldValue, newValue);
|
||||
|
||||
Messenger.Send(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
|
||||
/// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
|
||||
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
|
||||
/// are not raised if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
T oldValue = field;
|
||||
|
||||
// We duplicate the code as in the base class here to leverage
|
||||
// the intrinsics support for EqualityComparer<T>.Default.Equals.
|
||||
bool propertyChanged = SetProperty(ref field, newValue, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
{
|
||||
Broadcast(oldValue, newValue, propertyName);
|
||||
}
|
||||
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
|
||||
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, bool broadcast, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
T oldValue = field;
|
||||
|
||||
bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
{
|
||||
Broadcast(oldValue, newValue, propertyName);
|
||||
}
|
||||
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
|
||||
/// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
|
||||
/// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
|
||||
/// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
|
||||
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
|
||||
/// are not raised if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
{
|
||||
Broadcast(oldValue, newValue, propertyName);
|
||||
}
|
||||
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
|
||||
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
{
|
||||
Broadcast(oldValue, newValue, propertyName);
|
||||
}
|
||||
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given nested property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
|
||||
/// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>, with the difference being that this
|
||||
/// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="model">The model </param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
{
|
||||
Broadcast(oldValue, newValue, propertyName);
|
||||
}
|
||||
|
||||
return propertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given nested property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
|
||||
/// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>,
|
||||
/// with the difference being that this method is used to relay properties from a wrapped model in the
|
||||
/// current instance. For more info, see the docs for
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="model">The model </param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
{
|
||||
Broadcast(oldValue, newValue, propertyName);
|
||||
}
|
||||
|
||||
return propertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
// 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;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.ComponentModel
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for objects implementing the <see cref="INotifyDataErrorInfo"/> interface. This class
|
||||
/// also inherits from <see cref="ObservableObject"/>, so it can be used for observable items too.
|
||||
/// </summary>
|
||||
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Dictionary{TKey,TValue}"/> instance used to store previous validation results.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, List<ValidationResult>> errors = new Dictionary<string, List<ValidationResult>>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasErrors
|
||||
{
|
||||
get
|
||||
{
|
||||
// This uses the value enumerator for Dictionary<TKey, TValue>.ValueCollection, so it doesn't
|
||||
// allocate. Accessing this property is O(n), but we can stop as soon as we find at least one
|
||||
// error in the whole entity, and doing this saves 8 bytes in the object size (no fields needed).
|
||||
foreach (var value in this.errors.Values)
|
||||
{
|
||||
if (value.Count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
|
||||
/// of the <paramref name="validate"/> parameter. If that is set to <see langword="true"/>, the new value will be
|
||||
/// validated and <see cref="ErrorsChanged"/> will be raised if needed. Following the behavior of the base method,
|
||||
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
|
||||
/// are not raised if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(ref field, newValue, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
|
||||
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="field">The field storing the property's value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(ref field, newValue, comparer, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
|
||||
/// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
|
||||
/// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
|
||||
/// of the <paramref name="validate"/> parameter. As such, following the behavior of the base method,
|
||||
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
|
||||
/// are not raised if the current and new value for the target property are the same.
|
||||
/// </remarks>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
|
||||
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
|
||||
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property that changed.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, comparer, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given nested property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
|
||||
/// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>, with the difference being that this
|
||||
/// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="model">The model </param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, model, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the current and new values for a given nested property. If the value has changed,
|
||||
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
|
||||
/// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>,
|
||||
/// with the difference being that this method is used to relay properties from a wrapped model in the
|
||||
/// current instance. For more info, see the docs for
|
||||
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
|
||||
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
|
||||
/// <param name="oldValue">The current property value.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
|
||||
/// <param name="model">The model </param>
|
||||
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
|
||||
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string? propertyName = null)
|
||||
where TModel : class
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
ValidateProperty(newValue, propertyName);
|
||||
}
|
||||
|
||||
return SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public IEnumerable GetErrors(string? propertyName)
|
||||
{
|
||||
// Entity-level errors when the target property is null or empty
|
||||
if (string.IsNullOrEmpty(propertyName))
|
||||
{
|
||||
return this.GetAllErrors();
|
||||
}
|
||||
|
||||
// Property-level errors, if any
|
||||
if (this.errors.TryGetValue(propertyName!, out List<ValidationResult> errors))
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
// The INotifyDataErrorInfo.GetErrors method doesn't specify exactly what to
|
||||
// return when the input property name is invalid, but given that the return
|
||||
// type is marked as a non-nullable reference type, here we're returning an
|
||||
// empty array to respect the contract. This also matches the behavior of
|
||||
// this method whenever errors for a valid properties are retrieved.
|
||||
return Array.Empty<ValidationResult>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the logic for entity-level errors gathering for <see cref="GetErrors"/>.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable"/> instance with all the errors in <see cref="errors"/>.</returns>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private IEnumerable GetAllErrors()
|
||||
{
|
||||
return this.errors.Values.SelectMany(errors => errors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a property with a specified name and a given input value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to test for the specified property.</param>
|
||||
/// <param name="propertyName">The name of the property to validate.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is <see langword="null"/>.</exception>
|
||||
private void ValidateProperty(object? value, string? propertyName)
|
||||
{
|
||||
if (propertyName is null)
|
||||
{
|
||||
ThrowArgumentNullExceptionForNullPropertyName();
|
||||
}
|
||||
|
||||
// Check if the property had already been previously validated, and if so retrieve
|
||||
// the reusable list of validation errors from the errors dictionary. This list is
|
||||
// used to add new validation errors below, if any are produced by the validator.
|
||||
// If the property isn't present in the dictionary, add it now to avoid allocations.
|
||||
if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors))
|
||||
{
|
||||
propertyErrors = new List<ValidationResult>();
|
||||
|
||||
this.errors.Add(propertyName!, propertyErrors);
|
||||
}
|
||||
|
||||
bool errorsChanged = false;
|
||||
|
||||
// Clear the errors for the specified property, if any
|
||||
if (propertyErrors.Count > 0)
|
||||
{
|
||||
propertyErrors.Clear();
|
||||
|
||||
errorsChanged = true;
|
||||
}
|
||||
|
||||
// Validate the property, by adding new errors to the existing list
|
||||
bool isValid = Validator.TryValidateProperty(
|
||||
value,
|
||||
new ValidationContext(this, null, null) { MemberName = propertyName },
|
||||
propertyErrors);
|
||||
|
||||
// Only raise the event once if needed. This happens either when the target property
|
||||
// had existing errors and is now valid, or if the validation has failed and there are
|
||||
// new errors to broadcast, regardless of the previous validation state for the property.
|
||||
if (errorsChanged || !isValid)
|
||||
{
|
||||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable SA1204
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> when a property name given as input is <see langword="null"/>.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentNullExceptionForNullPropertyName()
|
||||
{
|
||||
throw new ArgumentNullException("propertyName", "The input property name cannot be null when validating a property");
|
||||
}
|
||||
#pragma warning restore SA1204
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// 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;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Toolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A command that mirrors the functionality of <see cref="RelayCommand"/>, with the addition of
|
||||
/// accepting a <see cref="Func{TResult}"/> returning a <see cref="Task"/> as the execute
|
||||
/// action, and providing an <see cref="ExecutionTask"/> property that notifies changes when
|
||||
/// <see cref="ExecuteAsync"/> is invoked and when the returned <see cref="Task"/> completes.
|
||||
/// </summary>
|
||||
public sealed class AsyncRelayCommand : ObservableObject, IAsyncRelayCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<Task>? execute;
|
||||
|
||||
/// <summary>
|
||||
/// The cancelable <see cref="Func{T,TResult}"/> to invoke when <see cref="Execute"/> is used.
|
||||
/// </summary>
|
||||
/// <remarks>Only one between this and <see cref="execute"/> is not <see langword="null"/>.</remarks>
|
||||
private readonly Func<CancellationToken, Task>? cancelableExecute;
|
||||
|
||||
/// <summary>
|
||||
/// The optional action to invoke when <see cref="CanExecute"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<bool>? canExecute;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> instance to use to cancel <see cref="cancelableExecute"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This is only used when <see cref="cancelableExecute"/> is not <see langword="null"/>.</remarks>
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
public AsyncRelayCommand(Func<Task> execute)
|
||||
{
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute)
|
||||
{
|
||||
this.cancelableExecute = cancelableExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute)
|
||||
{
|
||||
this.execute = execute;
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
|
||||
{
|
||||
this.cancelableExecute = cancelableExecute;
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
private TaskNotifier? executionTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task? ExecutionTask
|
||||
{
|
||||
get => this.executionTask;
|
||||
private set
|
||||
{
|
||||
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanBeCanceled => !(this.cancelableExecute is null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRunning => ExecutionTask?.IsCompleted == false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return this.canExecute?.Invoke() != false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
ExecuteAsync(parameter);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ExecuteAsync(object? parameter)
|
||||
{
|
||||
if (CanExecute(parameter))
|
||||
{
|
||||
// Non cancelable command delegate
|
||||
if (!(this.execute is null))
|
||||
{
|
||||
return ExecutionTask = this.execute();
|
||||
}
|
||||
|
||||
// Cancel the previous operation, if one is pending
|
||||
this.cancellationTokenSource?.Cancel();
|
||||
|
||||
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
|
||||
// Invoke the cancelable command delegate with a new linked token
|
||||
return ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Cancel()
|
||||
{
|
||||
this.cancellationTokenSource?.Cancel();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
// 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;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Toolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic command that provides a more specific version of <see cref="AsyncRelayCommand"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of parameter being passed as input to the callbacks.</typeparam>
|
||||
public sealed class AsyncRelayCommand<T> : ObservableObject, IAsyncRelayCommand<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute(T)"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<T, Task>? execute;
|
||||
|
||||
/// <summary>
|
||||
/// The cancelable <see cref="Func{T1,T2,TResult}"/> to invoke when <see cref="Execute(object?)"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<T, CancellationToken, Task>? cancelableExecute;
|
||||
|
||||
/// <summary>
|
||||
/// The optional action to invoke when <see cref="CanExecute(T)"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<T, bool>? canExecute;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> instance to use to cancel <see cref="cancelableExecute"/>.
|
||||
/// </summary>
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand{T}"/> class that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
|
||||
public AsyncRelayCommand(Func<T, Task> execute)
|
||||
{
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand{T}"/> class that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
|
||||
public AsyncRelayCommand(Func<T, CancellationToken, Task> cancelableExecute)
|
||||
{
|
||||
this.cancelableExecute = cancelableExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
|
||||
public AsyncRelayCommand(Func<T, Task> execute, Func<T, bool> canExecute)
|
||||
{
|
||||
this.execute = execute;
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cancelableExecute">The cancelable execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
|
||||
public AsyncRelayCommand(Func<T, CancellationToken, Task> cancelableExecute, Func<T, bool> canExecute)
|
||||
{
|
||||
this.cancelableExecute = cancelableExecute;
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
private TaskNotifier? executionTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task? ExecutionTask
|
||||
{
|
||||
get => this.executionTask;
|
||||
private set
|
||||
{
|
||||
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanBeCanceled => !(this.cancelableExecute is null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRunning => ExecutionTask?.IsCompleted == false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool CanExecute(T parameter)
|
||||
{
|
||||
return this.canExecute?.Invoke(parameter) != false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
if (typeof(T).IsValueType &&
|
||||
parameter is null &&
|
||||
this.canExecute is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return CanExecute((T)parameter!);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Execute(T parameter)
|
||||
{
|
||||
ExecuteAsync(parameter);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
ExecuteAsync((T)parameter!);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ExecuteAsync(T parameter)
|
||||
{
|
||||
if (CanExecute(parameter))
|
||||
{
|
||||
// Non cancelable command delegate
|
||||
if (!(this.execute is null))
|
||||
{
|
||||
return ExecutionTask = this.execute(parameter);
|
||||
}
|
||||
|
||||
// Cancel the previous operation, if one is pending
|
||||
this.cancellationTokenSource?.Cancel();
|
||||
|
||||
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
|
||||
// Invoke the cancelable command delegate with a new linked token
|
||||
return ExecutionTask = this.cancelableExecute!(parameter, cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ExecuteAsync(object? parameter)
|
||||
{
|
||||
return ExecuteAsync((T)parameter!);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Cancel()
|
||||
{
|
||||
this.cancellationTokenSource?.Cancel();
|
||||
|
||||
OnPropertyChanged(nameof(IsCancellationRequested));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// 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.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface expanding <see cref="IRelayCommand"/> to support asynchronous operations.
|
||||
/// </summary>
|
||||
public interface IAsyncRelayCommand : IRelayCommand, INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the last scheduled <see cref="Task"/>, if available.
|
||||
/// This property notifies a change when the <see cref="Task"/> completes.
|
||||
/// </summary>
|
||||
Task? ExecutionTask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether running operations for this command can be canceled.
|
||||
/// </summary>
|
||||
bool CanBeCanceled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a cancelation request has been issued for the current operation.
|
||||
/// </summary>
|
||||
bool IsCancellationRequested { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command currently has a pending operation being executed.
|
||||
/// </summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides a more specific version of <see cref="System.Windows.Input.ICommand.Execute"/>,
|
||||
/// also returning the <see cref="Task"/> representing the async operation being executed.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The input parameter.</param>
|
||||
/// <returns>The <see cref="Task"/> representing the async operation being executed.</returns>
|
||||
Task ExecuteAsync(object? parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Communicates a request for cancelation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the underlying command is not running, or if it does not support cancelation, this method will perform no action.
|
||||
/// Note that even with a successful cancelation, the completion of the current operation might not be immediate.
|
||||
/// </remarks>
|
||||
void Cancel();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// 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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic interface representing a more specific version of <see cref="IAsyncRelayCommand"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type used as argument for the interface methods.</typeparam>
|
||||
/// <remarks>This interface is needed to solve the diamond problem with base classes.</remarks>
|
||||
public interface IAsyncRelayCommand<in T> : IAsyncRelayCommand, IRelayCommand<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a strongly-typed variant of <see cref="IAsyncRelayCommand.ExecuteAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The input parameter.</param>
|
||||
/// <returns>The <see cref="Task"/> representing the async operation being executed.</returns>
|
||||
Task ExecuteAsync(T parameter);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface expanding <see cref="ICommand"/> with the ability to raise
|
||||
/// the <see cref="ICommand.CanExecuteChanged"/> event externally.
|
||||
/// </summary>
|
||||
public interface IRelayCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Notifies that the <see cref="ICommand.CanExecute"/> property has changed.
|
||||
/// </summary>
|
||||
void NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// 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.Windows.Input;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic interface representing a more specific version of <see cref="IRelayCommand"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type used as argument for the interface methods.</typeparam>
|
||||
public interface IRelayCommand<in T> : IRelayCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a strongly-typed variant of <see cref="ICommand.CanExecute(object)"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The input parameter.</param>
|
||||
/// <returns>Whether or not the current command can be executed.</returns>
|
||||
/// <remarks>Use this overload to avoid boxing, if <typeparamref name="T"/> is a value type.</remarks>
|
||||
bool CanExecute(T parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Provides a strongly-typed variant of <see cref="ICommand.Execute(object)"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The input parameter.</param>
|
||||
/// <remarks>Use this overload to avoid boxing, if <typeparamref name="T"/> is a value type.</remarks>
|
||||
void Execute(T parameter);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// 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.
|
||||
|
||||
#pragma warning disable SA1512
|
||||
|
||||
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A command whose sole purpose is to relay its functionality to other
|
||||
/// objects by invoking delegates. The default return value for the <see cref="CanExecute"/>
|
||||
/// method is <see langword="true"/>. This type does not allow you to accept command parameters
|
||||
/// in the <see cref="Execute"/> and <see cref="CanExecute"/> callback methods.
|
||||
/// </summary>
|
||||
public sealed class RelayCommand : IRelayCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Action"/> to invoke when <see cref="Execute"/> is used.
|
||||
/// </summary>
|
||||
private readonly Action execute;
|
||||
|
||||
/// <summary>
|
||||
/// The optional action to invoke when <see cref="CanExecute"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<bool>? canExecute;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand"/> class that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
public RelayCommand(Action execute)
|
||||
{
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
public RelayCommand(Action execute, Func<bool> canExecute)
|
||||
{
|
||||
this.execute = execute;
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return this.canExecute?.Invoke() != false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
if (CanExecute(parameter))
|
||||
{
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// 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.
|
||||
|
||||
#pragma warning disable SA1512
|
||||
|
||||
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic command whose sole purpose is to relay its functionality to other
|
||||
/// objects by invoking delegates. The default return value for the CanExecute
|
||||
/// method is <see langword="true"/>. This class allows you to accept command parameters
|
||||
/// in the <see cref="Execute(T)"/> and <see cref="CanExecute(T)"/> callback methods.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of parameter being passed as input to the callbacks.</typeparam>
|
||||
public sealed class RelayCommand<T> : IRelayCommand<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Action"/> to invoke when <see cref="Execute(T)"/> is used.
|
||||
/// </summary>
|
||||
private readonly Action<T> execute;
|
||||
|
||||
/// <summary>
|
||||
/// The optional action to invoke when <see cref="CanExecute(T)"/> is used.
|
||||
/// </summary>
|
||||
private readonly Func<T, bool>? canExecute;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand{T}"/> class that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <remarks>
|
||||
/// Due to the fact that the <see cref="System.Windows.Input.ICommand"/> interface exposes methods that accept a
|
||||
/// nullable <see cref="object"/> parameter, it is recommended that if <typeparamref name="T"/> is a reference type,
|
||||
/// you should always declare it as nullable, and to always perform checks within <paramref name="execute"/>.
|
||||
/// </remarks>
|
||||
public RelayCommand(Action<T> execute)
|
||||
{
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="execute">The execution logic.</param>
|
||||
/// <param name="canExecute">The execution status logic.</param>
|
||||
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
|
||||
public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
|
||||
{
|
||||
this.execute = execute;
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void NotifyCanExecuteChanged()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool CanExecute(T parameter)
|
||||
{
|
||||
return this.canExecute?.Invoke(parameter) != false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
if (typeof(T).IsValueType &&
|
||||
parameter is null &&
|
||||
this.canExecute is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return CanExecute((T)parameter!);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Execute(T parameter)
|
||||
{
|
||||
if (CanExecute(parameter))
|
||||
{
|
||||
this.execute(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
Execute((T)parameter!);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
// 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;
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see langword="delegate"/> used to represent actions to invoke when a message is received.
|
||||
/// The recipient is given as an input argument to allow message registrations to avoid creating
|
||||
/// closures: if an instance method on a recipient needs to be invoked it is possible to just
|
||||
/// cast the recipient to the right type and then access the local method from that instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <param name="recipient">The recipient that is receiving the message.</param>
|
||||
/// <param name="message">The message being received.</param>
|
||||
public delegate void MessageHandler<in TRecipient, in TMessage>(TRecipient recipient, TMessage message)
|
||||
where TRecipient : class
|
||||
where TMessage : class;
|
||||
|
||||
/// <summary>
|
||||
/// An interface for a type providing the ability to exchange messages between different objects.
|
||||
/// This can be useful to decouple different modules of an application without having to keep strong
|
||||
/// references to types being referenced. It is also possible to send messages to specific channels, uniquely
|
||||
/// identified by a token, and to have different messengers in different sections of an applications.
|
||||
/// In order to use the <see cref="IMessenger"/> functionalities, first define a message type, like so:
|
||||
/// <code>
|
||||
/// public sealed class LoginCompletedMessage { }
|
||||
/// </code>
|
||||
/// Then, register your a recipient for this message:
|
||||
/// <code>
|
||||
/// Messenger.Default.Register<MyRecipientType, LoginCompletedMessage>(this, (r, m) =>
|
||||
/// {
|
||||
/// // Handle the message here...
|
||||
/// });
|
||||
/// </code>
|
||||
/// The message handler here is a lambda expression taking two parameters: the recipient and the message.
|
||||
/// This is done to avoid the allocations for the closures that would've been generated if the expression
|
||||
/// had captured the current instance. The recipient type parameter is used so that the recipient can be
|
||||
/// directly accessed within the handler without the need to manually perform type casts. This allows the
|
||||
/// code to be less verbose and more reliable, as all the checks are done just at build time. If the handler
|
||||
/// is defined within the same type as the recipient, it is also possible to directly access private members.
|
||||
/// This allows the message handler to be a static method, which enables the C# compiler to perform a number
|
||||
/// of additional memory optimizations (such as caching the delegate, avoiding unnecessary memory allocations).
|
||||
/// Finally, send a message when needed, like so:
|
||||
/// <code>
|
||||
/// Messenger.Default.Send<LoginCompletedMessage>();
|
||||
/// </code>
|
||||
/// Additionally, the method group syntax can also be used to specify the message handler
|
||||
/// to invoke when receiving a message, if a method with the right signature is available
|
||||
/// in the current scope. This is helpful to keep the registration and handling logic separate.
|
||||
/// Following up from the previous example, consider a class having this method:
|
||||
/// <code>
|
||||
/// private static void Receive(MyRecipientType recipient, LoginCompletedMessage message)
|
||||
/// {
|
||||
/// // Handle the message there
|
||||
/// }
|
||||
/// </code>
|
||||
/// The registration can then be performed in a single line like so:
|
||||
/// <code>
|
||||
/// Messenger.Default.Register(this, Receive);
|
||||
/// </code>
|
||||
/// The C# compiler will automatically convert that expression to a <see cref="MessageHandler{TRecipient,TMessage}"/> instance
|
||||
/// compatible with <see cref="IMessengerExtensions.Register{TRecipient,TMessage}(IMessenger,TRecipient,MessageHandler{TRecipient,TMessage})"/>.
|
||||
/// This will also work if multiple overloads of that method are available, each handling a different
|
||||
/// message type: the C# compiler will automatically pick the right one for the current message type.
|
||||
/// It is also possible to register message handlers explicitly using the <see cref="IRecipient{TMessage}"/> interface.
|
||||
/// To do so, the recipient just needs to implement the interface and then call the
|
||||
/// <see cref="IMessengerExtensions.RegisterAll(IMessenger,object)"/> extension, which will automatically register
|
||||
/// all the handlers that are declared by the recipient type. Registration for individual handlers is supported as well.
|
||||
/// </summary>
|
||||
public interface IMessenger
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether or not a given recipient has already been registered for a message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to check for the given recipient.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to check the channel for.</typeparam>
|
||||
/// <param name="recipient">The target recipient to check the registration for.</param>
|
||||
/// <param name="token">The token used to identify the target channel to check.</param>
|
||||
/// <returns>Whether or not <paramref name="recipient"/> has already been registered for the specified message.</returns>
|
||||
[Pure]
|
||||
bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a recipient for a given type of message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <param name="token">A token used to determine the receiving channel to use.</param>
|
||||
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
|
||||
void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
|
||||
where TRecipient : class
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>;
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a recipient from all registered messages.
|
||||
/// </summary>
|
||||
/// <param name="recipient">The recipient to unregister.</param>
|
||||
/// <remarks>
|
||||
/// This method will unregister the target recipient across all channels.
|
||||
/// Use this method as an easy way to lose all references to a target recipient.
|
||||
/// If the recipient has no registered handler, this method does nothing.
|
||||
/// </remarks>
|
||||
void UnregisterAll(object recipient);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a recipient from all messages on a specific channel.
|
||||
/// </summary>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to unregister from.</typeparam>
|
||||
/// <param name="recipient">The recipient to unregister.</param>
|
||||
/// <param name="token">The token to use to identify which handlers to unregister.</param>
|
||||
/// <remarks>If the recipient has no registered handler, this method does nothing.</remarks>
|
||||
void UnregisterAll<TToken>(object recipient, TToken token)
|
||||
where TToken : IEquatable<TToken>;
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a recipient from messages of a given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to stop receiving.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to unregister from.</typeparam>
|
||||
/// <param name="recipient">The recipient to unregister.</param>
|
||||
/// <param name="token">The token to use to identify which handlers to unregister.</param>
|
||||
/// <remarks>If the recipient has no registered handler, this method does nothing.</remarks>
|
||||
void Unregister<TMessage, TToken>(object recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message of the specified type to all registered recipients.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to send.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="token">The token indicating what channel to use.</param>
|
||||
/// <returns>The message that was sent (ie. <paramref name="message"/>).</returns>
|
||||
TMessage Send<TMessage, TToken>(TMessage message, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>;
|
||||
|
||||
/// <summary>
|
||||
/// Performs a cleanup on the current messenger.
|
||||
/// Invoking this method does not unregister any of the currently registered
|
||||
/// recipient, and it can be used to perform cleanup operations such as
|
||||
/// trimming the internal data structures of a messenger implementation.
|
||||
/// </summary>
|
||||
void Cleanup();
|
||||
|
||||
/// <summary>
|
||||
/// Resets the <see cref="IMessenger"/> instance and unregisters all the existing recipients.
|
||||
/// </summary>
|
||||
void Reset();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
// 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;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IMessenger"/> type.
|
||||
/// </summary>
|
||||
public static class IMessengerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that acts as a container to load the <see cref="MethodInfo"/> instance linked to
|
||||
/// the <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/> method.
|
||||
/// This class is needed to avoid forcing the initialization code in the static constructor to run as soon as
|
||||
/// the <see cref="IMessengerExtensions"/> type is referenced, even if that is done just to use methods
|
||||
/// that do not actually require this <see cref="MethodInfo"/> instance to be available.
|
||||
/// We're effectively using this type to leverage the lazy loading of static constructors done by the runtime.
|
||||
/// </summary>
|
||||
private static class MethodInfos
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="MethodInfos"/> class.
|
||||
/// </summary>
|
||||
static MethodInfos()
|
||||
{
|
||||
RegisterIRecipient = (
|
||||
from methodInfo in typeof(IMessengerExtensions).GetMethods()
|
||||
where methodInfo.Name == nameof(Register) &&
|
||||
methodInfo.IsGenericMethod &&
|
||||
methodInfo.GetGenericArguments().Length == 2
|
||||
let parameters = methodInfo.GetParameters()
|
||||
where parameters.Length == 3 &&
|
||||
parameters[1].ParameterType.IsGenericType &&
|
||||
parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(IRecipient<>)
|
||||
select methodInfo).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
|
||||
/// </summary>
|
||||
public static readonly MethodInfo RegisterIRecipient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class that acts as a static container to associate a <see cref="ConditionalWeakTable{TKey,TValue}"/> instance to each
|
||||
/// <typeparamref name="TToken"/> type in use. This is done because we can only use a single type as key, but we need to track
|
||||
/// associations of each recipient type also across different communication channels, each identified by a token.
|
||||
/// Since the token is actually a compile-time parameter, we can use a wrapping class to let the runtime handle a different
|
||||
/// instance for each generic type instantiation. This lets us only worry about the recipient type being inspected.
|
||||
/// </summary>
|
||||
/// <typeparam name="TToken">The token indicating what channel to use.</typeparam>
|
||||
private static class DiscoveredRecipients<TToken>
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track the preloaded registration actions for each recipient.
|
||||
/// </summary>
|
||||
public static readonly ConditionalWeakTable<Type, Action<IMessenger, object, TToken>[]> RegistrationMethods
|
||||
= new ConditionalWeakTable<Type, Action<IMessenger, object, TToken>[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not a given recipient has already been registered for a message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to check for the given recipient.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to check the registration.</param>
|
||||
/// <param name="recipient">The target recipient to check the registration for.</param>
|
||||
/// <returns>Whether or not <paramref name="recipient"/> has already been registered for the specified message.</returns>
|
||||
/// <remarks>This method will use the default channel to check for the requested registration.</remarks>
|
||||
[Pure]
|
||||
public static bool IsRegistered<TMessage>(this IMessenger messenger, object recipient)
|
||||
where TMessage : class
|
||||
{
|
||||
return messenger.IsRegistered<TMessage, Unit>(recipient, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers all declared message handlers for a given recipient, using the default channel.
|
||||
/// </summary>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <remarks>See notes for <see cref="RegisterAll{TToken}(IMessenger,object,TToken)"/> for more info.</remarks>
|
||||
public static void RegisterAll(this IMessenger messenger, object recipient)
|
||||
{
|
||||
messenger.RegisterAll(recipient, default(Unit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers all declared message handlers for a given recipient.
|
||||
/// </summary>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to use to receive messages.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <param name="token">The token indicating what channel to use.</param>
|
||||
/// <remarks>
|
||||
/// This method will register all messages corresponding to the <see cref="IRecipient{TMessage}"/> interfaces
|
||||
/// being implemented by <paramref name="recipient"/>. If none are present, this method will do nothing.
|
||||
/// Note that unlike all other extensions, this method will use reflection to find the handlers to register.
|
||||
/// Once the registration is complete though, the performance will be exactly the same as with handlers
|
||||
/// registered directly through any of the other generic extensions for the <see cref="IMessenger"/> interface.
|
||||
/// </remarks>
|
||||
public static void RegisterAll<TToken>(this IMessenger messenger, object recipient, TToken token)
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
// We use this method as a callback for the conditional weak table, which will both
|
||||
// handle thread-safety for us, as well as avoiding all the LINQ codegen bloat here.
|
||||
// This method is only invoked once per recipient type and token type, so we're not
|
||||
// worried about making it super efficient, and we can use the LINQ code for clarity.
|
||||
static Action<IMessenger, object, TToken>[] LoadRegistrationMethodsForType(Type type)
|
||||
{
|
||||
return (
|
||||
from interfaceType in type.GetInterfaces()
|
||||
where interfaceType.IsGenericType &&
|
||||
interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>)
|
||||
let messageType = interfaceType.GenericTypeArguments[0]
|
||||
let registrationMethod = MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken))
|
||||
let registrationAction = GetRegistrationAction(type, registrationMethod)
|
||||
select registrationAction).ToArray();
|
||||
}
|
||||
|
||||
// Helper method to build and compile an expression tree to a message handler to use for the registration
|
||||
// This is used to reduce the overhead of repeated calls to MethodInfo.Invoke (which is over 10 times slower).
|
||||
static Action<IMessenger, object, TToken> GetRegistrationAction(Type type, MethodInfo methodInfo)
|
||||
{
|
||||
// Input parameters (IMessenger instance, non-generic recipient, token)
|
||||
ParameterExpression
|
||||
arg0 = Expression.Parameter(typeof(IMessenger)),
|
||||
arg1 = Expression.Parameter(typeof(object)),
|
||||
arg2 = Expression.Parameter(typeof(TToken));
|
||||
|
||||
// Cast the recipient and invoke the registration method
|
||||
MethodCallExpression body = Expression.Call(null, methodInfo, new Expression[]
|
||||
{
|
||||
arg0,
|
||||
Expression.Convert(arg1, type),
|
||||
arg2
|
||||
});
|
||||
|
||||
// Create the expression tree and compile to a target delegate
|
||||
return Expression.Lambda<Action<IMessenger, object, TToken>>(body, arg0, arg1, arg2).Compile();
|
||||
}
|
||||
|
||||
// Get or compute the registration methods for the current recipient type.
|
||||
// As in Microsoft.Toolkit.Extensions.TypeExtensions.ToTypeString, we use a lambda
|
||||
// expression instead of a method group expression to leverage the statically initialized
|
||||
// delegate and avoid repeated allocations for each invocation of this method.
|
||||
// For more info on this, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
|
||||
Action<IMessenger, object, TToken>[] registrationActions = DiscoveredRecipients<TToken>.RegistrationMethods.GetValue(
|
||||
recipient.GetType(),
|
||||
t => LoadRegistrationMethodsForType(t));
|
||||
|
||||
foreach (Action<IMessenger, object, TToken> registrationAction in registrationActions)
|
||||
{
|
||||
registrationAction(messenger, recipient, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a recipient for a given type of message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
|
||||
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
|
||||
public static void Register<TMessage>(this IMessenger messenger, IRecipient<TMessage> recipient)
|
||||
where TMessage : class
|
||||
{
|
||||
messenger.Register<IRecipient<TMessage>, TMessage, Unit>(recipient, default, (r, m) => r.Receive(m));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a recipient for a given type of message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to use to receive messages.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <param name="token">The token indicating what channel to use.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
|
||||
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
|
||||
public static void Register<TMessage, TToken>(this IMessenger messenger, IRecipient<TMessage> recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
messenger.Register<IRecipient<TMessage>, TMessage, TToken>(recipient, token, (r, m) => r.Receive(m));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a recipient for a given type of message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
|
||||
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
|
||||
public static void Register<TMessage>(this IMessenger messenger, object recipient, MessageHandler<object, TMessage> handler)
|
||||
where TMessage : class
|
||||
{
|
||||
messenger.Register(recipient, default(Unit), handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a recipient for a given type of message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
|
||||
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
|
||||
public static void Register<TRecipient, TMessage>(this IMessenger messenger, TRecipient recipient, MessageHandler<TRecipient, TMessage> handler)
|
||||
where TRecipient : class
|
||||
where TMessage : class
|
||||
{
|
||||
messenger.Register(recipient, default(Unit), handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a recipient for a given type of message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
|
||||
/// <param name="recipient">The recipient that will receive the messages.</param>
|
||||
/// <param name="token">A token used to determine the receiving channel to use.</param>
|
||||
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
|
||||
public static void Register<TMessage, TToken>(this IMessenger messenger, object recipient, TToken token, MessageHandler<object, TMessage> handler)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
messenger.Register(recipient, token, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a recipient from messages of a given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to stop receiving.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to unregister the recipient.</param>
|
||||
/// <param name="recipient">The recipient to unregister.</param>
|
||||
/// <remarks>
|
||||
/// This method will unregister the target recipient only from the default channel.
|
||||
/// If the recipient has no registered handler, this method does nothing.
|
||||
/// </remarks>
|
||||
public static void Unregister<TMessage>(this IMessenger messenger, object recipient)
|
||||
where TMessage : class
|
||||
{
|
||||
messenger.Unregister<TMessage, Unit>(recipient, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message of the specified type to all registered recipients.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to send.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
|
||||
/// <returns>The message that has been sent.</returns>
|
||||
/// <remarks>
|
||||
/// This method is a shorthand for <see cref="Send{TMessage}(IMessenger,TMessage)"/> when the
|
||||
/// message type exposes a parameterless constructor: it will automatically create
|
||||
/// a new <typeparamref name="TMessage"/> instance and send that to its recipients.
|
||||
/// </remarks>
|
||||
public static TMessage Send<TMessage>(this IMessenger messenger)
|
||||
where TMessage : class, new()
|
||||
{
|
||||
return messenger.Send(new TMessage(), default(Unit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message of the specified type to all registered recipients.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to send.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <returns>The message that was sent (ie. <paramref name="message"/>).</returns>
|
||||
public static TMessage Send<TMessage>(this IMessenger messenger, TMessage message)
|
||||
where TMessage : class
|
||||
{
|
||||
return messenger.Send(message, default(Unit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message of the specified type to all registered recipients.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to send.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
|
||||
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
|
||||
/// <param name="token">The token indicating what channel to use.</param>
|
||||
/// <returns>The message that has been sen.</returns>
|
||||
/// <remarks>
|
||||
/// This method will automatically create a new <typeparamref name="TMessage"/> instance
|
||||
/// just like <see cref="Send{TMessage}(IMessenger)"/>, and then send it to the right recipients.
|
||||
/// </remarks>
|
||||
public static TMessage Send<TMessage, TToken>(this IMessenger messenger, TToken token)
|
||||
where TMessage : class, new()
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
return messenger.Send(new TMessage(), token);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for a recipient that declares a registration for a specific message type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
public interface IRecipient<in TMessage>
|
||||
where TMessage : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Receives a given <typeparamref name="TMessage"/> message instance.
|
||||
/// </summary>
|
||||
/// <param name="message">The message being received.</param>
|
||||
void Receive(TMessage message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,421 @@
|
|||
// 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.
|
||||
|
||||
#pragma warning disable SA1512
|
||||
|
||||
// The DictionarySlim<TKey, TValue> type is originally from CoreFX labs, see
|
||||
// the source repository on GitHub at https://github.com/dotnet/corefxlab.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Collections.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A lightweight Dictionary with three principal differences compared to <see cref="Dictionary{TKey, TValue}"/>
|
||||
///
|
||||
/// 1) It is possible to do "get or add" in a single lookup. For value types, this also saves a copy of the value.
|
||||
/// 2) It assumes it is cheap to equate values.
|
||||
/// 3) It assumes the keys implement <see cref="IEquatable{TKey}"/> and they are cheap and sufficient.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
|
||||
/// <remarks>
|
||||
/// 1) This avoids having to do separate lookups (<see cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
|
||||
/// followed by <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/>.
|
||||
/// There is not currently an API exposed to get a value by ref without adding if the key is not present.
|
||||
/// 2) This means it can save space by not storing hash codes.
|
||||
/// 3) This means it can avoid storing a comparer, and avoid the likely virtual call to a comparer.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("Count = {Count}")]
|
||||
internal class DictionarySlim<TKey, TValue> : IDictionarySlim<TKey, TValue>
|
||||
where TKey : IEquatable<TKey>
|
||||
where TValue : class
|
||||
{
|
||||
/// <summary>
|
||||
/// A reusable array of <see cref="Entry"/> items with a single value.
|
||||
/// This is used when a new <see cref="DictionarySlim{TKey,TValue}"/> instance is
|
||||
/// created, or when one is cleared. The first item being added to this collection
|
||||
/// will immediately cause the first resize (see <see cref="AddKey"/> for more info).
|
||||
/// </summary>
|
||||
private static readonly Entry[] InitialEntries = new Entry[1];
|
||||
|
||||
/// <summary>
|
||||
/// The current number of items stored in the map.
|
||||
/// </summary>
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// The 1-based index for the start of the free list within <see cref="entries"/>.
|
||||
/// </summary>
|
||||
private int freeList = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The array of 1-based indices for the <see cref="Entry"/> items stored in <see cref="entries"/>.
|
||||
/// </summary>
|
||||
private int[] buckets;
|
||||
|
||||
/// <summary>
|
||||
/// The array of currently stored key-value pairs (ie. the lists for each hash group).
|
||||
/// </summary>
|
||||
private Entry[] entries;
|
||||
|
||||
/// <summary>
|
||||
/// A type representing a map entry, ie. a node in a given list.
|
||||
/// </summary>
|
||||
private struct Entry
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the value in the current node.
|
||||
/// </summary>
|
||||
public TKey Key;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the current node, if present.
|
||||
/// </summary>
|
||||
public TValue? Value;
|
||||
|
||||
/// <summary>
|
||||
/// The 0-based index for the next node in the current list.
|
||||
/// </summary>
|
||||
public int Next;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DictionarySlim{TKey, TValue}"/> class.
|
||||
/// </summary>
|
||||
public DictionarySlim()
|
||||
{
|
||||
this.buckets = HashHelpers.SizeOneIntArray;
|
||||
this.entries = InitialEntries;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => this.count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TValue this[TKey key]
|
||||
{
|
||||
get
|
||||
{
|
||||
Entry[] entries = this.entries;
|
||||
|
||||
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
|
||||
(uint)i < (uint)entries.Length;
|
||||
i = entries[i].Next)
|
||||
{
|
||||
if (key.Equals(entries[i].Key))
|
||||
{
|
||||
return entries[i].Value!;
|
||||
}
|
||||
}
|
||||
|
||||
ThrowArgumentExceptionForKeyNotFound(key);
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
this.count = 0;
|
||||
this.freeList = -1;
|
||||
this.buckets = HashHelpers.SizeOneIntArray;
|
||||
this.entries = InitialEntries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not the dictionary contains a pair with a specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to look for.</param>
|
||||
/// <returns>Whether or not the key was present in the dictionary.</returns>
|
||||
public bool ContainsKey(TKey key)
|
||||
{
|
||||
Entry[] entries = this.entries;
|
||||
|
||||
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
|
||||
(uint)i < (uint)entries.Length;
|
||||
i = entries[i].Next)
|
||||
{
|
||||
if (key.Equals(entries[i].Key))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value if present for the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to look for.</param>
|
||||
/// <param name="value">The value found, otherwise <see langword="default"/>.</param>
|
||||
/// <returns>Whether or not the key was present.</returns>
|
||||
public bool TryGetValue(TKey key, out TValue? value)
|
||||
{
|
||||
Entry[] entries = this.entries;
|
||||
|
||||
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
|
||||
(uint)i < (uint)entries.Length;
|
||||
i = entries[i].Next)
|
||||
{
|
||||
if (key.Equals(entries[i].Key))
|
||||
{
|
||||
value = entries[i].Value!;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default!;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryRemove(TKey key)
|
||||
{
|
||||
return TryRemove(key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to remove a value with a specified key, if present.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to remove.</param>
|
||||
/// <param name="result">The removed value, if it was present.</param>
|
||||
/// <returns>Whether or not the key was present.</returns>
|
||||
public bool TryRemove(TKey key, out TValue? result)
|
||||
{
|
||||
Entry[] entries = this.entries;
|
||||
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
|
||||
int entryIndex = this.buckets[bucketIndex] - 1;
|
||||
int lastIndex = -1;
|
||||
|
||||
while (entryIndex != -1)
|
||||
{
|
||||
Entry candidate = entries[entryIndex];
|
||||
|
||||
if (candidate.Key.Equals(key))
|
||||
{
|
||||
if (lastIndex != -1)
|
||||
{
|
||||
entries[lastIndex].Next = candidate.Next;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.buckets[bucketIndex] = candidate.Next + 1;
|
||||
}
|
||||
|
||||
entries[entryIndex] = default;
|
||||
entries[entryIndex].Next = -3 - this.freeList;
|
||||
|
||||
this.freeList = entryIndex;
|
||||
this.count--;
|
||||
|
||||
result = candidate.Value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
lastIndex = entryIndex;
|
||||
entryIndex = candidate.Next;
|
||||
}
|
||||
|
||||
result = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value for the specified key, or, if the key is not present,
|
||||
/// adds an entry and returns the value by ref. This makes it possible to
|
||||
/// add or update a value in a single look up operation.
|
||||
/// </summary>
|
||||
/// <param name="key">Key to look for</param>
|
||||
/// <returns>Reference to the new or existing value</returns>
|
||||
public ref TValue? GetOrAddValueRef(TKey key)
|
||||
{
|
||||
Entry[] entries = this.entries;
|
||||
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
|
||||
|
||||
for (int i = this.buckets[bucketIndex] - 1;
|
||||
(uint)i < (uint)entries.Length;
|
||||
i = entries[i].Next)
|
||||
{
|
||||
if (key.Equals(entries[i].Key))
|
||||
{
|
||||
return ref entries[i].Value;
|
||||
}
|
||||
}
|
||||
|
||||
return ref AddKey(key, bucketIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a slot for a new value to add for a specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to use to add the new value.</param>
|
||||
/// <param name="bucketIndex">The target bucked index to use.</param>
|
||||
/// <returns>A reference to the slot for the new value to add.</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private ref TValue? AddKey(TKey key, int bucketIndex)
|
||||
{
|
||||
Entry[] entries = this.entries;
|
||||
int entryIndex;
|
||||
|
||||
if (this.freeList != -1)
|
||||
{
|
||||
entryIndex = this.freeList;
|
||||
|
||||
this.freeList = -3 - entries[this.freeList].Next;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.count == entries.Length || entries.Length == 1)
|
||||
{
|
||||
entries = Resize();
|
||||
bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
|
||||
}
|
||||
|
||||
entryIndex = this.count;
|
||||
}
|
||||
|
||||
entries[entryIndex].Key = key;
|
||||
entries[entryIndex].Next = this.buckets[bucketIndex] - 1;
|
||||
|
||||
this.buckets[bucketIndex] = entryIndex + 1;
|
||||
this.count++;
|
||||
|
||||
return ref entries[entryIndex].Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes the current dictionary to reduce the number of collisions
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private Entry[] Resize()
|
||||
{
|
||||
int count = this.count;
|
||||
int newSize = this.entries.Length * 2;
|
||||
|
||||
if ((uint)newSize > int.MaxValue)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForMaxCapacityExceeded();
|
||||
}
|
||||
|
||||
var entries = new Entry[newSize];
|
||||
|
||||
Array.Copy(this.entries, 0, entries, 0, count);
|
||||
|
||||
var newBuckets = new int[entries.Length];
|
||||
|
||||
while (count-- > 0)
|
||||
{
|
||||
int bucketIndex = entries[count].Key.GetHashCode() & (newBuckets.Length - 1);
|
||||
|
||||
entries[count].Next = newBuckets[bucketIndex] - 1;
|
||||
|
||||
newBuckets[bucketIndex] = count + 1;
|
||||
}
|
||||
|
||||
this.buckets = newBuckets;
|
||||
this.entries = entries;
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||
|
||||
/// <summary>
|
||||
/// Enumerator for <see cref="DictionarySlim{TKey,TValue}"/>.
|
||||
/// </summary>
|
||||
public ref struct Enumerator
|
||||
{
|
||||
private readonly Entry[] entries;
|
||||
private int index;
|
||||
private int count;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal Enumerator(DictionarySlim<TKey, TValue> dictionary)
|
||||
{
|
||||
this.entries = dictionary.entries;
|
||||
this.index = 0;
|
||||
this.count = dictionary.count;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerator.MoveNext"/>
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.count--;
|
||||
|
||||
Entry[] entries = this.entries;
|
||||
|
||||
while (entries[this.index].Next < -1)
|
||||
{
|
||||
this.index++;
|
||||
}
|
||||
|
||||
// We need to preemptively increment the current index so that we still correctly keep track
|
||||
// of the current position in the dictionary even if the users doesn't access any of the
|
||||
// available properties in the enumerator. As this is a possibility, we can't rely on one of
|
||||
// them to increment the index before MoveNext is invoked again. We ditch the standard enumerator
|
||||
// API surface here to expose the Key/Value properties directly and minimize the memory copies.
|
||||
// For the same reason, we also removed the KeyValuePair<TKey, TValue> field here, and instead
|
||||
// rely on the properties lazily accessing the target instances directly from the current entry
|
||||
// pointed at by the index property (adjusted backwards to account for the increment here).
|
||||
this.index++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current key.
|
||||
/// </summary>
|
||||
public TKey Key
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.entries[this.index - 1].Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value.
|
||||
/// </summary>
|
||||
public TValue Value
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.entries[this.index - 1].Value!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> when trying to load an element with a missing key.
|
||||
/// </summary>
|
||||
private static void ThrowArgumentExceptionForKeyNotFound(TKey key)
|
||||
{
|
||||
throw new ArgumentException($"The target key {key} was not present in the dictionary");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when trying to resize over the maximum capacity.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForMaxCapacityExceeded()
|
||||
{
|
||||
throw new InvalidOperationException("Max capacity exceeded");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.Collections.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class for <see cref="DictionarySlim{TKey,TValue}"/>.
|
||||
/// </summary>
|
||||
internal static class HashHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// An array of type <see cref="int"/> of size 1.
|
||||
/// </summary>
|
||||
public static readonly int[] SizeOneIntArray = new int[1];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.Collections.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A base interface masking <see cref="DictionarySlim{TKey,TValue}"/> instances and exposing non-generic functionalities.
|
||||
/// </summary>
|
||||
internal interface IDictionarySlim
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the count of entries in the dictionary.
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current dictionary.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Collections.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface providing key type contravariant and value type covariant access
|
||||
/// to a <see cref="DictionarySlim{TKey,TValue}"/> instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The contravariant type of keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">The covariant type of values in the dictionary.</typeparam>
|
||||
internal interface IDictionarySlim<in TKey, out TValue> : IDictionarySlim<TKey>
|
||||
where TKey : IEquatable<TKey>
|
||||
where TValue : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to look for.</param>
|
||||
/// <returns>The returned value.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the key wasn't present.</exception>
|
||||
TValue this[TKey key] { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Collections.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface providing key type contravariant access to a <see cref="DictionarySlim{TKey,TValue}"/> instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The contravariant type of keys in the dictionary.</typeparam>
|
||||
internal interface IDictionarySlim<in TKey> : IDictionarySlim
|
||||
where TKey : IEquatable<TKey>
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to remove a value with a specified key, if present.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to remove.</param>
|
||||
/// <returns>Whether or not the key was present.</returns>
|
||||
bool TryRemove(TKey key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// 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;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple type representing an immutable pair of types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type replaces a simple <see cref="ValueTuple{T1,T2}"/> as it's faster in its
|
||||
/// <see cref="GetHashCode"/> and <see cref="IEquatable{T}.Equals(T)"/> methods, and because
|
||||
/// unlike a value tuple it exposes its fields as immutable. Additionally, the
|
||||
/// <see cref="TMessage"/> and <see cref="TToken"/> fields provide additional clarity reading
|
||||
/// the code compared to <see cref="ValueTuple{T1,T2}.Item1"/> and <see cref="ValueTuple{T1,T2}.Item2"/>.
|
||||
/// </remarks>
|
||||
internal readonly struct Type2 : IEquatable<Type2>
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of registered message.
|
||||
/// </summary>
|
||||
public readonly Type TMessage;
|
||||
|
||||
/// <summary>
|
||||
/// The type of registration token.
|
||||
/// </summary>
|
||||
public readonly Type TToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Type2"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="tMessage">The type of registered message.</param>
|
||||
/// <param name="tToken">The type of registration token.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Type2(Type tMessage, Type tToken)
|
||||
{
|
||||
TMessage = tMessage;
|
||||
TToken = tToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(Type2 other)
|
||||
{
|
||||
// We can't just use reference equality, as that's technically not guaranteed
|
||||
// to work and might fail in very rare cases (eg. with type forwarding between
|
||||
// different assemblies). Instead, we can use the == operator to compare for
|
||||
// equality, which still avoids the callvirt overhead of calling Type.Equals,
|
||||
// and is also implemented as a JIT intrinsic on runtimes such as .NET Core.
|
||||
return
|
||||
TMessage == other.TMessage &&
|
||||
TToken == other.TToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Type2 other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
|
||||
// This is not a problem in this case since we already know that the base
|
||||
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
|
||||
int hash = RuntimeHelpers.GetHashCode(TMessage);
|
||||
|
||||
hash = (hash << 5) + hash;
|
||||
|
||||
hash += RuntimeHelpers.GetHashCode(TToken);
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// 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;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Internals
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty type representing a generic token with no specific value.
|
||||
/// </summary>
|
||||
internal readonly struct Unit : IEquatable<Unit>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(Unit other)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Unit;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see langword="class"/> for request messages that can receive multiple replies, which can either be used directly or through derived classes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of request to make.</typeparam>
|
||||
public class AsyncCollectionRequestMessage<T> : IAsyncEnumerable<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The collection of received replies. We accept both <see cref="Task{TResult}"/> instance, representing already running
|
||||
/// operations that can be executed in parallel, or <see cref="Func{T,TResult}"/> instances, which can be used so that multiple
|
||||
/// asynchronous operations are only started sequentially from <see cref="GetAsyncEnumerator"/> and do not overlap in time.
|
||||
/// </summary>
|
||||
private readonly List<(Task<T>?, Func<CancellationToken, Task<T>>?)> responses = new List<(Task<T>?, Func<CancellationToken, Task<T>>?)>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> instance used to link the token passed to
|
||||
/// <see cref="GetAsyncEnumerator"/> and the one passed to all subscribers to the message.
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="System.Threading.CancellationToken"/> instance that will be linked to the
|
||||
/// one used to asynchronously enumerate the received responses. This can be used to cancel asynchronous
|
||||
/// replies that are still being processed, if no new items are needed from this request message.
|
||||
/// Consider the following example, where we define a message to retrieve the currently opened documents:
|
||||
/// <code>
|
||||
/// public class OpenDocumentsRequestMessage : AsyncCollectionRequestMessage<XmlDocument> { }
|
||||
/// </code>
|
||||
/// We can then request and enumerate the results like so:
|
||||
/// <code>
|
||||
/// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>())
|
||||
/// {
|
||||
/// // Process each document here...
|
||||
/// }
|
||||
/// </code>
|
||||
/// If we also want to control the cancellation of the token passed to each subscriber to the message,
|
||||
/// we can do so by passing a token we control to the returned message before starting the enumeration
|
||||
/// (<see cref="TaskAsyncEnumerableExtensions.WithCancellation{T}(IAsyncEnumerable{T},CancellationToken)"/>).
|
||||
/// The previous snippet with this additional change looks as follows:
|
||||
/// <code>
|
||||
/// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>().WithCancellation(cts.Token))
|
||||
/// {
|
||||
/// // Process each document here...
|
||||
/// }
|
||||
/// </code>
|
||||
/// When no more new items are needed (or for any other reason depending on the situation), the token
|
||||
/// passed to the enumerator can be canceled (by calling <see cref="CancellationTokenSource.Cancel()"/>),
|
||||
/// and that will also notify the remaining tasks in the request message. The token exposed by the message
|
||||
/// itself will automatically be linked and canceled with the one passed to the enumerator.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken => this.cancellationTokenSource.Token;
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
public void Reply(T response)
|
||||
{
|
||||
Reply(Task.FromResult(response));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
public void Reply(Task<T> response)
|
||||
{
|
||||
this.responses.Add((response, null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
public void Reply(Func<CancellationToken, Task<T>> response)
|
||||
{
|
||||
this.responses.Add((null, response));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of received response items.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="System.Threading.CancellationToken"/> value to stop the operation.</param>
|
||||
/// <returns>The collection of received response items.</returns>
|
||||
[Pure]
|
||||
public async Task<IReadOnlyCollection<T>> GetResponsesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationToken.Register(this.cancellationTokenSource.Cancel);
|
||||
}
|
||||
|
||||
List<T> results = new List<T>(this.responses.Count);
|
||||
|
||||
await foreach (var response in this.WithCancellation(cancellationToken))
|
||||
{
|
||||
results.Add(response);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationToken.Register(this.cancellationTokenSource.Cancel);
|
||||
}
|
||||
|
||||
foreach (var (task, func) in this.responses)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!(task is null))
|
||||
{
|
||||
yield return await task.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return await func!(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
// 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;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see langword="class"/> for async request messages, which can either be used directly or through derived classes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of request to make.</typeparam>
|
||||
public class AsyncRequestMessage<T>
|
||||
{
|
||||
private Task<T>? response;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message response.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when <see cref="HasReceivedResponse"/> is <see langword="false"/>.</exception>
|
||||
public Task<T> Response
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasReceivedResponse)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForNoResponseReceived();
|
||||
}
|
||||
|
||||
return this.response!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a response has already been assigned to this instance.
|
||||
/// </summary>
|
||||
public bool HasReceivedResponse { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if <see cref="Response"/> has already been set.</exception>
|
||||
public void Reply(T response)
|
||||
{
|
||||
Reply(Task.FromResult(response));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if <see cref="Response"/> has already been set.</exception>
|
||||
public void Reply(Task<T> response)
|
||||
{
|
||||
if (HasReceivedResponse)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForDuplicateReply();
|
||||
}
|
||||
|
||||
HasReceivedResponse = true;
|
||||
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Task{T}.GetAwaiter"/>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public TaskAwaiter<T> GetAwaiter()
|
||||
{
|
||||
return this.Response.GetAwaiter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when a response is not available.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForNoResponseReceived()
|
||||
{
|
||||
throw new InvalidOperationException("No response was received for the given request message");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when <see cref="Reply(T)"/> or <see cref="Reply(Task{T})"/> are called twice.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForDuplicateReply()
|
||||
{
|
||||
throw new InvalidOperationException("A response has already been issued for the current message");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see langword="class"/> for request messages that can receive multiple replies, which can either be used directly or through derived classes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of request to make.</typeparam>
|
||||
public class CollectionRequestMessage<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly List<T> responses = new List<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message responses.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<T> Responses => this.responses;
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
public void Reply(T response)
|
||||
{
|
||||
this.responses.Add(response);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return this.responses.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// 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.
|
||||
|
||||
#pragma warning disable SA1512
|
||||
|
||||
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// A message used to broadcast property changes in observable objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property to broadcast the change for.</typeparam>
|
||||
public class PropertyChangedMessage<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PropertyChangedMessage{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sender">The original sender of the broadcast message.</param>
|
||||
/// <param name="propertyName">The name of the property that changed.</param>
|
||||
/// <param name="oldValue">The value that the property had before the change.</param>
|
||||
/// <param name="newValue">The value that the property has after the change.</param>
|
||||
public PropertyChangedMessage(object sender, string? propertyName, T oldValue, T newValue)
|
||||
{
|
||||
Sender = sender;
|
||||
PropertyName = propertyName;
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original sender of the broadcast message.
|
||||
/// </summary>
|
||||
public object Sender { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the property that changed.
|
||||
/// </summary>
|
||||
public string? PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value that the property had before the change.
|
||||
/// </summary>
|
||||
public T OldValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value that the property has after the change.
|
||||
/// </summary>
|
||||
public T NewValue { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// 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;
|
||||
|
||||
#pragma warning disable CS8618
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see langword="class"/> for request messages, which can either be used directly or through derived classes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of request to make.</typeparam>
|
||||
public class RequestMessage<T>
|
||||
{
|
||||
private T response;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message response.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when <see cref="HasReceivedResponse"/> is <see langword="false"/>.</exception>
|
||||
public T Response
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasReceivedResponse)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForNoResponseReceived();
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a response has already been assigned to this instance.
|
||||
/// </summary>
|
||||
public bool HasReceivedResponse { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Replies to the current request message.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to use to reply to the request message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if <see cref="Response"/> has already been set.</exception>
|
||||
public void Reply(T response)
|
||||
{
|
||||
if (HasReceivedResponse)
|
||||
{
|
||||
ThrowInvalidOperationExceptionForDuplicateReply();
|
||||
}
|
||||
|
||||
HasReceivedResponse = true;
|
||||
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly gets the response from a given <see cref="RequestMessage{T}"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="message">The input <see cref="RequestMessage{T}"/> instance.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when <see cref="HasReceivedResponse"/> is <see langword="false"/>.</exception>
|
||||
public static implicit operator T(RequestMessage<T> message)
|
||||
{
|
||||
return message.Response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when a response is not available.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForNoResponseReceived()
|
||||
{
|
||||
throw new InvalidOperationException("No response was received for the given request message");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when <see cref="Reply"/> is called twice.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForDuplicateReply()
|
||||
{
|
||||
throw new InvalidOperationException("A response has already been issued for the current message");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// A base message that signals whenever a specific value has changed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value that has changed.</typeparam>
|
||||
public class ValueChangedMessage<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValueChangedMessage{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The value that has changed.</param>
|
||||
public ValueChangedMessage(T value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value that has changed.
|
||||
/// </summary>
|
||||
public T Value { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,579 @@
|
|||
// 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;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Microsoft.Collections.Extensions;
|
||||
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging
|
||||
{
|
||||
/// <summary>
|
||||
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This <see cref="IMessenger"/> implementation uses strong references to track the registered
|
||||
/// recipients, so it is necessary to manually unregister them when they're no longer needed.
|
||||
/// </remarks>
|
||||
public sealed class StrongReferenceMessenger : IMessenger
|
||||
{
|
||||
// This messenger uses the following logic to link stored instances together:
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
// DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap;
|
||||
// | \________________[*]IDictionarySlim<Recipient, IDictionarySlim<TToken>>
|
||||
// | \____________/_________ /
|
||||
// | ________(recipients registrations)____________________/ \ /
|
||||
// | / ____(channel registrations)________________\____________/
|
||||
// | / / \
|
||||
// DictionarySlim<Recipient, DictionarySlim<TToken, MessageHandler<TRecipient, TMessage>>> mapping = Mapping<TMessage, TToken>
|
||||
// / / /
|
||||
// ___(Type2.TToken)____/ / /
|
||||
// /________________(Type2.TMessage)________________________/ /
|
||||
// / ____________________________________________________________/
|
||||
// / /
|
||||
// DictionarySlim<Type2, IMapping> typesMap;
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds the references
|
||||
// from registered recipients to handlers. The handlers are stored in a <TToken, MessageHandler<object, TMessage>> dictionary,
|
||||
// so that each recipient can have up to one registered handler for a given token, for each message type.
|
||||
// Note that the registered handlers are only stored as object references, even if they were actually of type
|
||||
// MessageHandler<TRecipient, TMessage>, to avoid unnecessary unsafe casts. Each handler is also generic with respect to the
|
||||
// recipient type, in order to allow the messenger to track and invoke type-specific handlers without using reflection and
|
||||
// without having to capture the input handler in a proxy delegate, causing one extra memory allocations and adding overhead.
|
||||
// This allows users to retain type information on each registered recipient, instead of having to manually cast each recipient
|
||||
// to the right type within the handler. The type conversion is guaranteed to be respected due to how the messenger type
|
||||
// itself works - as registered handlers are always invoked on their respective recipients.
|
||||
// Each mapping is stored in the types map, which associates each pair of concrete types to its
|
||||
// mapping instance. Mapping instances are exposed as IMapping items, as each will be a closed type over
|
||||
// a different combination of TMessage and TToken generic type parameters. Each existing recipient is also stored in
|
||||
// the main recipients map, along with a set of all the existing dictionaries of handlers for that recipient (for all
|
||||
// message types and token types). A recipient is stored in the main map as long as it has at least one
|
||||
// registered handler in any of the existing mappings for every message/token type combination.
|
||||
// The shared map is used to access the set of all registered handlers for a given recipient, without having
|
||||
// to know in advance the type of message or token being used for the registration, and without having to
|
||||
// use reflection. This is the same approach used in the types map, as we expose saved items as IMapping values too.
|
||||
// Note that each mapping stored in the associated set for each recipient also indirectly implements
|
||||
// IDictionarySlim<Recipient, Token>, with any token type currently in use by that recipient. This allows to retrieve
|
||||
// the type-closed mappings of registered handlers with a given token type, for any message type, for every receiver,
|
||||
// again without having to use reflection. This shared map is used to unregister messages from a given recipients
|
||||
// either unconditionally, by message type, by token, or for a specific pair of message type and token value.
|
||||
|
||||
/// <summary>
|
||||
/// The collection of currently registered recipients, with a link to their linked message receivers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This collection is used to allow reflection-free access to all the existing
|
||||
/// registered recipients from <see cref="UnregisterAll"/> and other methods in this type,
|
||||
/// so that all the existing handlers can be removed without having to dynamically create
|
||||
/// the generic types for the containers of the various dictionaries mapping the handlers.
|
||||
/// </remarks>
|
||||
private readonly DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap = new DictionarySlim<Recipient, HashSet<IMapping>>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Mapping{TMessage,TToken}"/> instance for types combination.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values are just of type <see cref="IDictionarySlim{T}"/> as we don't know the type parameters in advance.
|
||||
/// Each method relies on <see cref="GetOrAddMapping{TMessage,TToken}"/> to get the type-safe instance
|
||||
/// of the <see cref="Mapping{TMessage,TToken}"/> class for each pair of generic arguments in use.
|
||||
/// </remarks>
|
||||
private readonly DictionarySlim<Type2, IMapping> typesMap = new DictionarySlim<Type2, IMapping>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="StrongReferenceMessenger"/> instance.
|
||||
/// </summary>
|
||||
public static StrongReferenceMessenger Default { get; } = new StrongReferenceMessenger();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = new Recipient(recipient);
|
||||
|
||||
return mapping!.ContainsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
|
||||
where TRecipient : class
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
// Get the <TMessage, TToken> registration list for this recipient
|
||||
Mapping<TMessage, TToken> mapping = GetOrAddMapping<TMessage, TToken>();
|
||||
var key = new Recipient(recipient);
|
||||
ref DictionarySlim<TToken, object>? map = ref mapping.GetOrAddValueRef(key);
|
||||
|
||||
map ??= new DictionarySlim<TToken, object>();
|
||||
|
||||
// Add the new registration entry
|
||||
ref object? registeredHandler = ref map.GetOrAddValueRef(token);
|
||||
|
||||
if (!(registeredHandler is null))
|
||||
{
|
||||
ThrowInvalidOperationExceptionForDuplicateRegistration();
|
||||
}
|
||||
|
||||
// Treat the input delegate as if it was covariant (see comments below in the Send method)
|
||||
registeredHandler = handler;
|
||||
|
||||
// Make sure this registration map is tracked for the current recipient
|
||||
ref HashSet<IMapping>? set = ref this.recipientsMap.GetOrAddValueRef(key);
|
||||
|
||||
set ??= new HashSet<IMapping>();
|
||||
|
||||
set.Add(mapping);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterAll(object recipient)
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
// If the recipient has no registered messages at all, ignore
|
||||
var key = new Recipient(recipient);
|
||||
|
||||
if (!this.recipientsMap.TryGetValue(key, out HashSet<IMapping>? set))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Removes all the lists of registered handlers for the recipient
|
||||
foreach (IMapping mapping in set!)
|
||||
{
|
||||
if (mapping.TryRemove(key) &&
|
||||
mapping.Count == 0)
|
||||
{
|
||||
// Maps here are really of type Mapping<,> and with unknown type arguments.
|
||||
// If after removing the current recipient a given map becomes empty, it means
|
||||
// that there are no registered recipients at all for a given pair of message
|
||||
// and token types. In that case, we also remove the map from the types map.
|
||||
// The reason for keeping a key in each mapping is that removing items from a
|
||||
// dictionary (a hashed collection) only costs O(1) in the best case, while
|
||||
// if we had tried to iterate the whole dictionary every time we would have
|
||||
// paid an O(n) minimum cost for each single remove operation.
|
||||
this.typesMap.TryRemove(mapping.TypeArguments, out _);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the associated set in the recipients map
|
||||
this.recipientsMap.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterAll<TToken>(object recipient, TToken token)
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
bool lockTaken = false;
|
||||
object[]? maps = null;
|
||||
int i = 0;
|
||||
|
||||
// We use an explicit try/finally block here instead of the lock syntax so that we can use a single
|
||||
// one both to release the lock and to clear the rented buffer and return it to the pool. The reason
|
||||
// why we're declaring the buffer here and clearing and returning it in this outer finally block is
|
||||
// that doing so doesn't require the lock to be kept, and releasing it before performing this last
|
||||
// step reduces the total time spent while the lock is acquired, which in turn reduces the lock
|
||||
// contention in multi-threaded scenarios where this method is invoked concurrently.
|
||||
try
|
||||
{
|
||||
Monitor.Enter(this.recipientsMap, ref lockTaken);
|
||||
|
||||
// Get the shared set of mappings for the recipient, if present
|
||||
var key = new Recipient(recipient);
|
||||
|
||||
if (!this.recipientsMap.TryGetValue(key, out HashSet<IMapping>? set))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the candidate mappings for the target recipient to a local array, as we can't modify the
|
||||
// contents of the set while iterating it. The rented buffer is oversized and will also include
|
||||
// mappings for handlers of messages that are registered through a different token. Note that
|
||||
// we're using just an object array to minimize the number of total rented buffers, that would
|
||||
// just remain in the shared pool unused, other than when they are rented here. Instead, we're
|
||||
// using a type that would possibly also be used by the users of the library, which increases
|
||||
// the opportunities to reuse existing buffers for both. When we need to reference an item
|
||||
// stored in the buffer with the type we know it will have, we use Unsafe.As<T> to avoid the
|
||||
// expensive type check in the cast, since we already know the assignment will be valid.
|
||||
maps = ArrayPool<object>.Shared.Rent(set!.Count);
|
||||
|
||||
foreach (IMapping item in set)
|
||||
{
|
||||
// Select all mappings using the same token type
|
||||
if (item is IDictionarySlim<Recipient, IDictionarySlim<TToken>> mapping)
|
||||
{
|
||||
maps[i++] = mapping;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all the local maps. These are all the currently
|
||||
// existing maps of handlers for messages of any given type, with a token
|
||||
// of the current type, for the target recipient. We heavily rely on
|
||||
// interfaces here to be able to iterate through all the available mappings
|
||||
// without having to know the concrete type in advance, and without having
|
||||
// to deal with reflection: we can just check if the type of the closed interface
|
||||
// matches with the token type currently in use, and operate on those instances.
|
||||
foreach (object obj in maps.AsSpan(0, i))
|
||||
{
|
||||
var map = Unsafe.As<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>(obj);
|
||||
|
||||
// We don't need whether or not the map contains the recipient, as the
|
||||
// sequence of maps has already been copied from the set containing all
|
||||
// the mappings for the target recipients: it is guaranteed to be here.
|
||||
IDictionarySlim<TToken> holder = map[key];
|
||||
|
||||
// Try to remove the registered handler for the input token,
|
||||
// for the current message type (unknown from here).
|
||||
if (holder.TryRemove(token) &&
|
||||
holder.Count == 0)
|
||||
{
|
||||
// If the map is empty, remove the recipient entirely from its container
|
||||
map.TryRemove(key);
|
||||
|
||||
// If no handlers are left at all for the recipient, across all
|
||||
// message types and token types, remove the set of mappings
|
||||
// entirely for the current recipient, and lost the strong
|
||||
// reference to it as well. This is the same situation that
|
||||
// would've been achieved by just calling UnregisterAll(recipient).
|
||||
if (map.Count == 0 &&
|
||||
set.Remove(Unsafe.As<IMapping>(map)) &&
|
||||
set.Count == 0)
|
||||
{
|
||||
this.recipientsMap.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the lock, if we did acquire it
|
||||
if (lockTaken)
|
||||
{
|
||||
Monitor.Exit(this.recipientsMap);
|
||||
}
|
||||
|
||||
// If we got to renting the array of maps, return it to the shared pool.
|
||||
// Remove references to avoid leaks coming from the shared memory pool.
|
||||
// We manually create a span and clear it as a small optimization, as
|
||||
// arrays rented from the pool can be larger than the requested size.
|
||||
if (!(maps is null))
|
||||
{
|
||||
maps.AsSpan(0, i).Clear();
|
||||
|
||||
ArrayPool<object>.Shared.Return(maps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Unregister<TMessage, TToken>(object recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
// Get the registration list, if available
|
||||
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = new Recipient(recipient);
|
||||
|
||||
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, object>? dictionary))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the target handler
|
||||
if (dictionary!.TryRemove(token, out _) &&
|
||||
dictionary.Count == 0)
|
||||
{
|
||||
// If the map is empty, it means that the current recipient has no remaining
|
||||
// registered handlers for the current <TMessage, TToken> combination, regardless,
|
||||
// of the specific token value (ie. the channel used to receive messages of that type).
|
||||
// We can remove the map entirely from this container, and remove the link to the map itself
|
||||
// to the current mapping between existing registered recipients (or entire recipients too).
|
||||
mapping.TryRemove(key, out _);
|
||||
|
||||
HashSet<IMapping> set = this.recipientsMap[key];
|
||||
|
||||
if (set.Remove(mapping) &&
|
||||
set.Count == 0)
|
||||
{
|
||||
this.recipientsMap.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
object[] rentedArray;
|
||||
Span<object> pairs;
|
||||
int i = 0;
|
||||
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
// Check whether there are any registered recipients
|
||||
_ = TryGetMapping(out Mapping<TMessage, TToken>? mapping);
|
||||
|
||||
// We need to make a local copy of the currently registered handlers, since users might
|
||||
// try to unregister (or register) new handlers from inside one of the currently existing
|
||||
// handlers. We can use memory pooling to reuse arrays, to minimize the average memory
|
||||
// usage. In practice, we usually just need to pay the small overhead of copying the items.
|
||||
// The current mapping contains all the currently registered recipients and handlers for
|
||||
// the <TMessage, TToken> combination in use. In the worst case scenario, all recipients
|
||||
// will have a registered handler with a token matching the input one, meaning that we could
|
||||
// have at worst a number of pending handlers to invoke equal to the total number of recipient
|
||||
// in the mapping. This relies on the fact that tokens are unique, and that there is only
|
||||
// one handler associated with a given token. We can use this upper bound as the requested
|
||||
// size for each array rented from the pool, which guarantees that we'll have enough space.
|
||||
int totalHandlersCount = mapping?.Count ?? 0;
|
||||
|
||||
if (totalHandlersCount == 0)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
// Rent the array and also assign it to a span, which will be used to access values.
|
||||
// We're doing this to avoid the array covariance checks slowdown in the loops below.
|
||||
pairs = rentedArray = ArrayPool<object>.Shared.Rent(2 * totalHandlersCount);
|
||||
|
||||
// Copy the handlers to the local collection.
|
||||
// The array is oversized at this point, since it also includes
|
||||
// handlers for different tokens. We can reuse the same variable
|
||||
// to count the number of matching handlers to invoke later on.
|
||||
// This will be the array slice with valid handler in the rented buffer.
|
||||
var mappingEnumerator = mapping!.GetEnumerator();
|
||||
|
||||
// Explicit enumerator usage here as we're using a custom one
|
||||
// that doesn't expose the single standard Current property.
|
||||
while (mappingEnumerator.MoveNext())
|
||||
{
|
||||
object recipient = mappingEnumerator.Key.Target;
|
||||
|
||||
// Pick the target handler, if the token is a match for the recipient
|
||||
if (mappingEnumerator.Value.TryGetValue(token, out object? handler))
|
||||
{
|
||||
// This span access should always guaranteed to be valid due to the size of the
|
||||
// array being set according to the current total number of registered handlers,
|
||||
// which will always be greater or equal than the ones matching the previous test.
|
||||
// We're still using a checked span accesses here though to make sure an out of
|
||||
// bounds write can never happen even if an error was present in the logic above.
|
||||
pairs[2 * i] = handler!;
|
||||
pairs[(2 * i) + 1] = recipient;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Invoke all the necessary handlers on the local copy of entries
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
// Here we perform an unsafe cast to enable covariance for delegate types.
|
||||
// We know that the input recipient will always respect the type constraints
|
||||
// of each original input delegate, and doing so allows us to still invoke
|
||||
// them all from here without worrying about specific generic type arguments.
|
||||
Unsafe.As<MessageHandler<object, TMessage>>(pairs[2 * j])(pairs[(2 * j) + 1], message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// As before, we also need to clear it first to avoid having potentially long
|
||||
// lasting memory leaks due to leftover references being stored in the pool.
|
||||
Array.Clear(rentedArray, 0, 2 * i);
|
||||
|
||||
ArrayPool<object>.Shared.Return(rentedArray);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IMessenger.Cleanup()
|
||||
{
|
||||
// The current implementation doesn't require any kind of cleanup operation, as
|
||||
// all the internal data structures are already kept in sync whenever a recipient
|
||||
// is added or removed. This method is implemented through an explicit interface
|
||||
// implementation so that developers using this type directly will not see it in
|
||||
// the API surface (as it wouldn't be useful anyway, since it's a no-op here).
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reset()
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
this.recipientsMap.Clear();
|
||||
this.typesMap.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the <see cref="Mapping{TMessage,TToken}"/> instance of currently registered recipients
|
||||
/// for the combination of types <typeparamref name="TMessage"/> and <typeparamref name="TToken"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to send.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
|
||||
/// <param name="mapping">The resulting <see cref="Mapping{TMessage,TToken}"/> instance, if found.</param>
|
||||
/// <returns>Whether or not the required <see cref="Mapping{TMessage,TToken}"/> instance was found.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryGetMapping<TMessage, TToken>(out Mapping<TMessage, TToken>? mapping)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
var key = new Type2(typeof(TMessage), typeof(TToken));
|
||||
|
||||
if (this.typesMap.TryGetValue(key, out IMapping? target))
|
||||
{
|
||||
// This method and the ones above are the only ones handling values in the types map,
|
||||
// and here we are sure that the object reference we have points to an instance of the
|
||||
// right type. Using an unsafe cast skips two conditional branches and is faster.
|
||||
mapping = Unsafe.As<Mapping<TMessage, TToken>>(target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
mapping = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Mapping{TMessage,TToken}"/> instance of currently registered recipients
|
||||
/// for the combination of types <typeparamref name="TMessage"/> and <typeparamref name="TToken"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to send.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
|
||||
/// <returns>A <see cref="Mapping{TMessage,TToken}"/> instance with the requested type arguments.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Mapping<TMessage, TToken> GetOrAddMapping<TMessage, TToken>()
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
var key = new Type2(typeof(TMessage), typeof(TToken));
|
||||
ref IMapping? target = ref this.typesMap.GetOrAddValueRef(key);
|
||||
|
||||
target ??= new Mapping<TMessage, TToken>();
|
||||
|
||||
return Unsafe.As<Mapping<TMessage, TToken>>(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mapping type representing a link to recipients and their view of handlers per communication channel.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
|
||||
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
|
||||
/// <remarks>
|
||||
/// This type is defined for simplicity and as a workaround for the lack of support for using type aliases
|
||||
/// over open generic types in C# (using type aliases can only be used for concrete, closed types).
|
||||
/// </remarks>
|
||||
private sealed class Mapping<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, object>>, IMapping
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Mapping{TMessage, TToken}"/> class.
|
||||
/// </summary>
|
||||
public Mapping()
|
||||
{
|
||||
TypeArguments = new Type2(typeof(TMessage), typeof(TToken));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type2 TypeArguments { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An interface for the <see cref="Mapping{TMessage,TToken}"/> type which allows to retrieve the type
|
||||
/// arguments from a given generic instance without having any prior knowledge about those arguments.
|
||||
/// </summary>
|
||||
private interface IMapping : IDictionarySlim<Recipient>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Type2"/> instance representing the current type arguments.
|
||||
/// </summary>
|
||||
Type2 TypeArguments { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple type representing a recipient.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type is used to enable fast indexing in each mapping dictionary,
|
||||
/// since it acts as an external override for the <see cref="GetHashCode"/> and
|
||||
/// <see cref="Equals(object?)"/> methods for arbitrary objects, removing both
|
||||
/// the virtual call and preventing instances overriding those methods in this context.
|
||||
/// Using this type guarantees that all the equality operations are always only done
|
||||
/// based on reference equality for each registered recipient, regardless of its type.
|
||||
/// </remarks>
|
||||
private readonly struct Recipient : IEquatable<Recipient>
|
||||
{
|
||||
/// <summary>
|
||||
/// The registered recipient.
|
||||
/// </summary>
|
||||
public readonly object Target;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Recipient"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="target">The target recipient instance.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Recipient(object target)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(Recipient other)
|
||||
{
|
||||
return ReferenceEquals(Target, other.Target);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Recipient other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return RuntimeHelpers.GetHashCode(this.Target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when trying to add a duplicate handler.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
|
||||
{
|
||||
throw new InvalidOperationException("The target recipient has already subscribed to the target message");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,485 @@
|
|||
// 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;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Collections.Extensions;
|
||||
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
|
||||
#if NETSTANDARD2_1
|
||||
using RecipientsTable = System.Runtime.CompilerServices.ConditionalWeakTable<object, Microsoft.Collections.Extensions.IDictionarySlim>;
|
||||
#else
|
||||
using RecipientsTable = Microsoft.Toolkit.Mvvm.Messaging.WeakReferenceMessenger.ConditionalWeakTable<object, Microsoft.Collections.Extensions.IDictionarySlim>;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Toolkit.Mvvm.Messaging
|
||||
{
|
||||
/// <summary>
|
||||
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This <see cref="IMessenger"/> implementation uses weak references to track the registered
|
||||
/// recipients, so it is not necessary to manually unregister them when they're no longer needed.
|
||||
/// </remarks>
|
||||
public sealed class WeakReferenceMessenger : IMessenger
|
||||
{
|
||||
// This messenger uses the following logic to link stored instances together:
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
// DictionarySlim<TToken, MessageHandler<TRecipient, TMessage>> mapping
|
||||
// / / /
|
||||
// ___(Type2.TToken)___/ / /
|
||||
// /_________________(Type2.TMessage)______________________/ /
|
||||
// / ___________________________/
|
||||
// / /
|
||||
// DictionarySlim<Type2, ConditionalWeakTable<object, IDictionarySlim>> recipientsMap;
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
// Just like in the strong reference variant, each pair of message and token types is used as a key in the
|
||||
// recipients map. In this case, the values in the dictionary are ConditionalWeakTable<,> instances, that
|
||||
// link each registered recipient to a map of currently registered handlers, through a weak reference.
|
||||
// The value in each conditional table is Dictionary<TToken, MessageHandler<TRecipient, TMessage>>, using
|
||||
// the same unsafe cast as before to allow the generic handler delegates to be invoked without knowing
|
||||
// what type each recipient was registered with, and without the need to use reflection.
|
||||
|
||||
/// <summary>
|
||||
/// The map of currently registered recipients for all message types.
|
||||
/// </summary>
|
||||
private readonly DictionarySlim<Type2, RecipientsTable> recipientsMap = new DictionarySlim<Type2, RecipientsTable>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="WeakReferenceMessenger"/> instance.
|
||||
/// </summary>
|
||||
public static WeakReferenceMessenger Default { get; } = new WeakReferenceMessenger();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
Type2 type2 = new Type2(typeof(TMessage), typeof(TToken));
|
||||
|
||||
// Get the conditional table associated with the target recipient, for the current pair
|
||||
// of token and message types. If it exists, check if there is a matching token.
|
||||
return
|
||||
this.recipientsMap.TryGetValue(type2, out RecipientsTable? table) &&
|
||||
table!.TryGetValue(recipient, out IDictionarySlim? mapping) &&
|
||||
Unsafe.As<DictionarySlim<TToken, object>>(mapping).ContainsKey(token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
|
||||
where TRecipient : class
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
Type2 type2 = new Type2(typeof(TMessage), typeof(TToken));
|
||||
|
||||
// Get the conditional table for the pair of type arguments, or create it if it doesn't exist
|
||||
ref RecipientsTable? mapping = ref this.recipientsMap.GetOrAddValueRef(type2);
|
||||
|
||||
mapping ??= new RecipientsTable();
|
||||
|
||||
// Get or create the handlers dictionary for the target recipient
|
||||
var map = Unsafe.As<DictionarySlim<TToken, object>>(mapping.GetValue(recipient, _ => new DictionarySlim<TToken, object>()));
|
||||
|
||||
// Add the new registration entry
|
||||
ref object? registeredHandler = ref map.GetOrAddValueRef(token);
|
||||
|
||||
if (!(registeredHandler is null))
|
||||
{
|
||||
ThrowInvalidOperationExceptionForDuplicateRegistration();
|
||||
}
|
||||
|
||||
// Store the input handler
|
||||
registeredHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterAll(object recipient)
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
var enumerator = this.recipientsMap.GetEnumerator();
|
||||
|
||||
// Traverse all the existing conditional tables and remove all the ones
|
||||
// with the target recipient as key. We don't perform a cleanup here,
|
||||
// as that is responsability of a separate method defined below.
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
enumerator.Value.Remove(recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterAll<TToken>(object recipient, TToken token)
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
var enumerator = this.recipientsMap.GetEnumerator();
|
||||
|
||||
// Same as above, with the difference being that this time we only go through
|
||||
// the conditional tables having a matching token type as key, and that we
|
||||
// only try to remove handlers with a matching token, if any.
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (enumerator.Key.TToken == typeof(TToken) &&
|
||||
enumerator.Value.TryGetValue(recipient, out IDictionarySlim mapping))
|
||||
{
|
||||
Unsafe.As<DictionarySlim<TToken, object>>(mapping).TryRemove(token, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Unregister<TMessage, TToken>(object recipient, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
var type2 = new Type2(typeof(TMessage), typeof(TToken));
|
||||
var enumerator = this.recipientsMap.GetEnumerator();
|
||||
|
||||
// Traverse all the existing token and message pairs matching the current type
|
||||
// arguments, and remove all the handlers with a matching token, as above.
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (enumerator.Key.Equals(type2) &&
|
||||
enumerator.Value.TryGetValue(recipient, out IDictionarySlim mapping))
|
||||
{
|
||||
Unsafe.As<DictionarySlim<TToken, object>>(mapping).TryRemove(token, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
|
||||
where TMessage : class
|
||||
where TToken : IEquatable<TToken>
|
||||
{
|
||||
ArrayPoolBufferWriter<object> bufferWriter;
|
||||
int i = 0;
|
||||
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
Type2 type2 = new Type2(typeof(TMessage), typeof(TToken));
|
||||
|
||||
// Try to get the target table
|
||||
if (!this.recipientsMap.TryGetValue(type2, out RecipientsTable? table))
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
bufferWriter = ArrayPoolBufferWriter<object>.Create();
|
||||
|
||||
// We need a local, temporary copy of all the pending recipients and handlers to
|
||||
// invoke, to avoid issues with handlers unregistering from messages while we're
|
||||
// holding the lock. To do this, we can just traverse the conditional table in use
|
||||
// to enumerate all the existing recipients for the token and message types pair
|
||||
// corresponding to the generic arguments for this invocation, and then track the
|
||||
// handlers with a matching token, and their corresponding recipients.
|
||||
foreach (KeyValuePair<object, IDictionarySlim> pair in table!)
|
||||
{
|
||||
var map = Unsafe.As<DictionarySlim<TToken, object>>(pair.Value);
|
||||
|
||||
if (map.TryGetValue(token, out object? handler))
|
||||
{
|
||||
bufferWriter.Add(handler!);
|
||||
bufferWriter.Add(pair.Key);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ReadOnlySpan<object> pairs = bufferWriter.Span;
|
||||
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
// Just like in the other messenger, here we need an unsafe cast to be able to
|
||||
// invoke a generic delegate with a contravariant input argument, with a less
|
||||
// derived reference, without reflection. This is guaranteed to work by how the
|
||||
// messenger tracks registered recipients and their associated handlers, so the
|
||||
// type conversion will always be valid (the recipients are the rigth instances).
|
||||
Unsafe.As<MessageHandler<object, TMessage>>(pairs[2 * j])(pairs[(2 * j) + 1], message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
bufferWriter.Dispose();
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Cleanup()
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
using ArrayPoolBufferWriter<Type2> type2s = ArrayPoolBufferWriter<Type2>.Create();
|
||||
using ArrayPoolBufferWriter<object> emptyRecipients = ArrayPoolBufferWriter<object>.Create();
|
||||
|
||||
var enumerator = this.recipientsMap.GetEnumerator();
|
||||
|
||||
// First, we go through all the currently registered pairs of token and message types.
|
||||
// These represents all the combinations of generic arguments with at least one registered
|
||||
// handler, with the exception of those with recipients that have already been collected.
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
emptyRecipients.Reset();
|
||||
|
||||
bool hasAtLeastOneHandler = false;
|
||||
|
||||
// Go through the currently alive recipients to look for those with no handlers left. We track
|
||||
// the ones we find to remove them outside of the loop (can't modify during enumeration).
|
||||
foreach (KeyValuePair<object, IDictionarySlim> pair in enumerator.Value)
|
||||
{
|
||||
if (pair.Value.Count == 0)
|
||||
{
|
||||
emptyRecipients.Add(pair.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
hasAtLeastOneHandler = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the handler maps for recipients that are still alive but with no handlers
|
||||
foreach (object recipient in emptyRecipients.Span)
|
||||
{
|
||||
enumerator.Value.Remove(recipient);
|
||||
}
|
||||
|
||||
// Track the type combinations with no recipients or handlers left
|
||||
if (!hasAtLeastOneHandler)
|
||||
{
|
||||
type2s.Add(enumerator.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all the mappings with no handlers left
|
||||
foreach (Type2 key in type2s.Span)
|
||||
{
|
||||
this.recipientsMap.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reset()
|
||||
{
|
||||
lock (this.recipientsMap)
|
||||
{
|
||||
this.recipientsMap.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
#if !NETSTANDARD2_1
|
||||
/// <summary>
|
||||
/// A wrapper for <see cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}"/>
|
||||
/// that backports the enumerable support to .NET Standard 2.0 through an auxiliary list.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">Tke key of items to store in the table.</typeparam>
|
||||
/// <typeparam name="TValue">The values to store in the table.</typeparam>
|
||||
internal sealed class ConditionalWeakTable<TKey, TValue>
|
||||
where TKey : class
|
||||
where TValue : class?
|
||||
{
|
||||
/// <summary>
|
||||
/// The underlying <see cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}"/> instance.
|
||||
/// </summary>
|
||||
private readonly System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue> table;
|
||||
|
||||
/// <summary>
|
||||
/// A supporting linked list to store keys in <see cref="table"/>. This is needed to expose
|
||||
/// the ability to enumerate existing keys when there is no support for that in the BCL.
|
||||
/// </summary>
|
||||
private readonly LinkedList<WeakReference<TKey>> keys;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConditionalWeakTable{TKey, TValue}"/> class.
|
||||
/// </summary>
|
||||
public ConditionalWeakTable()
|
||||
{
|
||||
this.table = new System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>();
|
||||
this.keys = new LinkedList<WeakReference<TKey>>();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}.TryGetValue"/>
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
{
|
||||
return this.table.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}.GetValue"/>
|
||||
public TValue GetValue(TKey key, System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>.CreateValueCallback createValueCallback)
|
||||
{
|
||||
// Get or create the value. When this method returns, the key will be present in the table
|
||||
TValue value = this.table.GetValue(key, createValueCallback);
|
||||
|
||||
// Check if the list of keys contains the given key.
|
||||
// If it does, we can just stop here and return the result.
|
||||
foreach (WeakReference<TKey> node in this.keys)
|
||||
{
|
||||
if (node.TryGetTarget(out TKey? target) &&
|
||||
ReferenceEquals(target, key))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the key to the list of weak references to track it
|
||||
this.keys.AddFirst(new WeakReference<TKey>(key));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}.Remove"/>
|
||||
public bool Remove(TKey key)
|
||||
{
|
||||
return this.table.Remove(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
|
||||
{
|
||||
for (LinkedListNode<WeakReference<TKey>>? node = this.keys.First; !(node is null);)
|
||||
{
|
||||
LinkedListNode<WeakReference<TKey>>? next = node.Next;
|
||||
|
||||
// Get the key and value for the current node
|
||||
if (node.Value.TryGetTarget(out TKey? target) &&
|
||||
this.table.TryGetValue(target!, out TValue value))
|
||||
{
|
||||
yield return new KeyValuePair<TKey, TValue>(target, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the current key has been collected, trim the list
|
||||
this.keys.Remove(node);
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// A simple buffer writer implementation using pooled arrays.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items to store in the list.</typeparam>
|
||||
/// <remarks>
|
||||
/// This type is a <see langword="ref"/> <see langword="struct"/> to avoid the object allocation and to
|
||||
/// enable the pattern-based <see cref="IDisposable"/> support. We aren't worried with consumers not
|
||||
/// using this type correctly since it's private and only accessible within the parent type.
|
||||
/// </remarks>
|
||||
private ref struct ArrayPoolBufferWriter<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The default buffer size to use to expand empty arrays.
|
||||
/// </summary>
|
||||
private const int DefaultInitialBufferSize = 128;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying <typeparamref name="T"/> array.
|
||||
/// </summary>
|
||||
private T[] array;
|
||||
|
||||
/// <summary>
|
||||
/// The starting offset within <see cref="array"/>.
|
||||
/// </summary>
|
||||
private int index;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> struct.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ArrayPoolBufferWriter<T> Create()
|
||||
{
|
||||
return new ArrayPoolBufferWriter<T> { array = ArrayPool<T>.Shared.Rent(DefaultInitialBufferSize) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ReadOnlySpan{T}"/> with the current items.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<T> Span
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => this.array.AsSpan(0, this.index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new item to the current collection.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to add.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(T item)
|
||||
{
|
||||
if (this.index == this.array.Length)
|
||||
{
|
||||
ResizeBuffer();
|
||||
}
|
||||
|
||||
this.array[this.index++] = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the underlying array and the stored items.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(this.array, 0, this.index);
|
||||
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes <see cref="array"/> when there is no space left for new items.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ResizeBuffer()
|
||||
{
|
||||
T[] rent = ArrayPool<T>.Shared.Rent(this.index << 2);
|
||||
|
||||
Array.Copy(this.array, 0, rent, 0, this.index);
|
||||
Array.Clear(this.array, 0, this.index);
|
||||
|
||||
ArrayPool<T>.Shared.Return(this.array);
|
||||
|
||||
this.array = rent;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDisposable.Dispose"/>
|
||||
public void Dispose()
|
||||
{
|
||||
Array.Clear(this.array, 0, this.index);
|
||||
|
||||
ArrayPool<T>.Shared.Return(this.array);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> when trying to add a duplicate handler.
|
||||
/// </summary>
|
||||
private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
|
||||
{
|
||||
throw new InvalidOperationException("The target recipient has already subscribed to the target message");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Title>Windows Community Toolkit MVVM Toolkit</Title>
|
||||
<Description>
|
||||
This package includes a .NET Standard MVVM library with helpers such as:
|
||||
- ObservableObject: a base class for objects implementing the INotifyPropertyChanged interface.
|
||||
- ObservableRecipient: a base class for observable objects with support for the IMessenger service.
|
||||
- RelayCommand: a simple delegate command implementing the ICommand interface.
|
||||
- Messenger: a messaging system to exchange messages through different loosely-coupled objects.
|
||||
- Ioc: a helper class to configure dependency injection service containers.
|
||||
</Description>
|
||||
<PackageTags>UWP Toolkit Windows MVVM MVVMToolkit observable Ioc dependency injection services extensions helpers</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- .NET Standard 2.0 doesn't have the Span<T> and IAsyncEnumerable<T> types -->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.1.1" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- .NET Standard 2.1 doesn't have the Unsafe type -->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -27,7 +27,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Language specified in prefix, e.g. ```c# (Github Style Parsing).<para/>
|
||||
/// Gets or sets the Language specified in prefix, e.g. ```c# (GitHub Style Parsing).<para/>
|
||||
/// This does not guarantee that the Code Block has a language, or no language, some valid code might not have been prefixed, and this will still return null. <para/>
|
||||
/// To ensure all Code is Highlighted (If desired), you might have to determine the language from the provided string, such as looking for key words.
|
||||
/// </summary>
|
||||
|
|
|
@ -149,7 +149,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
}
|
||||
|
||||
russianDollIndex = Math.Min(russianDollIndex, (spaceCount - 1) / 4);
|
||||
int linestart = Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + ((russianDollIndex + 1) * 4));
|
||||
int lineStart = Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + ((russianDollIndex + 1) * 4));
|
||||
|
||||
// 0 spaces = end of the list.
|
||||
// 1-4 spaces = first level.
|
||||
|
@ -177,20 +177,20 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
builder.Builder.AppendLine();
|
||||
}
|
||||
|
||||
AppendTextToListItem(currentListItem, markdown, linestart, lineInfo.EndOfLine);
|
||||
AppendTextToListItem(currentListItem, markdown, lineStart, lineInfo.EndOfLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inline text. Ignores the 4 spaces that are used to continue the list.
|
||||
AppendTextToListItem(currentListItem, markdown, linestart, lineInfo.EndOfLine, true);
|
||||
AppendTextToListItem(currentListItem, markdown, lineStart, lineInfo.EndOfLine, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Closing Code Blocks.
|
||||
if (currentListItem.Blocks.Last() is ListItemBuilder currentBlock)
|
||||
{
|
||||
var blockmatchcount = Regex.Matches(currentBlock.Builder.ToString(), "```").Count;
|
||||
if (blockmatchcount > 0 && blockmatchcount % 2 != 0)
|
||||
var blockMatchCount = Regex.Matches(currentBlock.Builder.ToString(), "```").Count;
|
||||
if (blockMatchCount > 0 && blockMatchCount % 2 != 0)
|
||||
{
|
||||
inCodeBlock = true;
|
||||
}
|
||||
|
|
|
@ -296,7 +296,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
/// the block should find the start of the block, find the end and parse out the middle. The end most of the time will not be
|
||||
/// the max ending pos, but it sometimes can be. The function will return where it ended parsing the block in the markdown.
|
||||
/// </summary>
|
||||
/// <returns>the postiion parsed to</returns>
|
||||
/// <returns>the position parsed to</returns>
|
||||
internal int Parse(string markdown, int startingPos, int maxEndingPos, int quoteDepth)
|
||||
{
|
||||
Cells = new List<TableCell>();
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
/// <param name="markdown"> The markdown text. </param>
|
||||
/// <param name="start"> The location of the first hash character. </param>
|
||||
/// <param name="end"> The location of the end of the line. </param>
|
||||
/// <param name="realEndIndex"> The location of the actual end of the aprse. </param>
|
||||
/// <param name="realEndIndex"> The location of the actual end of the parse. </param>
|
||||
/// <returns>Parsed <see cref="YamlHeaderBlock"/> class</returns>
|
||||
internal static YamlHeaderBlock Parse(string markdown, int start, int end, out int realEndIndex)
|
||||
{
|
||||
|
@ -78,7 +78,6 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
else if (end - pos >= 3 && markdown.Substring(pos, 3) == "---")
|
||||
{
|
||||
lockedFinalUnderline = true;
|
||||
realEndIndex = pos + 3;
|
||||
break;
|
||||
}
|
||||
else if (startOfNextLine == pos + 1)
|
||||
|
@ -134,6 +133,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Blocks
|
|||
return null;
|
||||
}
|
||||
|
||||
realEndIndex = pos + 3;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -268,7 +268,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Helpers
|
|||
int remainingCount = markdown.Length - startingPos;
|
||||
if (count > remainingCount)
|
||||
{
|
||||
DebuggingReporter.ReportCriticalError("IndexOf count > remaing count");
|
||||
DebuggingReporter.ReportCriticalError("IndexOf count > remaining count");
|
||||
count = remainingCount;
|
||||
}
|
||||
|
||||
|
@ -306,7 +306,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Helpers
|
|||
int remainingCount = markdown.Length - startingPos;
|
||||
if (count > remainingCount)
|
||||
{
|
||||
DebuggingReporter.ReportCriticalError("IndexOf count > remaing count");
|
||||
DebuggingReporter.ReportCriticalError("IndexOf count > remaining count");
|
||||
count = remainingCount;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
public partial class EmojiInline
|
||||
{
|
||||
// Codes taken from https://gist.github.com/rxaviers/7360908
|
||||
// Ignoring not implented symbols in Segoe UI Emoji font (e.g. :bowtie:)
|
||||
// Ignoring not implemented symbols in Segoe UI Emoji font (e.g. :bowtie:)
|
||||
private static readonly Dictionary<string, int> _emojiCodesDictionary = new Dictionary<string, int>
|
||||
{
|
||||
{ "smile", 0x1f604 },
|
||||
|
|
|
@ -325,7 +325,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
// reddit (for example: '$' and '!').
|
||||
|
||||
// Special characters as per https://en.wikipedia.org/wiki/Email_address#Local-part allowed
|
||||
char[] allowedchars = new char[] { '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~' };
|
||||
char[] allowedChars = { '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~' };
|
||||
|
||||
int start = tripPos;
|
||||
while (start > minStart)
|
||||
|
@ -334,7 +334,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
if ((c < 'a' || c > 'z') &&
|
||||
(c < 'A' || c > 'Z') &&
|
||||
(c < '0' || c > '9') &&
|
||||
!allowedchars.Contains(c))
|
||||
!allowedChars.Contains(c))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
|
||||
if (pos < end && markdown[pos] == '[')
|
||||
{
|
||||
int refstart = pos;
|
||||
int refStart = pos;
|
||||
|
||||
// Find the reference ']' character
|
||||
while (pos < end)
|
||||
|
@ -131,7 +131,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
pos++;
|
||||
}
|
||||
|
||||
reference = markdown.Substring(refstart + 1, pos - refstart - 1);
|
||||
reference = markdown.Substring(refStart + 1, pos - refStart - 1);
|
||||
}
|
||||
else if (pos < end && markdown[pos] == '(')
|
||||
{
|
||||
|
@ -156,10 +156,10 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
if (imageDimensionsPos > 0)
|
||||
{
|
||||
// trying to find 'x' which separates image width and height
|
||||
var dimensionsSepatorPos = markdown.IndexOf("x", imageDimensionsPos + 2, pos - imageDimensionsPos - 1, StringComparison.Ordinal);
|
||||
var dimensionsSeparatorPos = markdown.IndexOf("x", imageDimensionsPos + 2, pos - imageDimensionsPos - 1, StringComparison.Ordinal);
|
||||
|
||||
// didn't find separator, trying to parse value as imageWidth
|
||||
if (dimensionsSepatorPos == -1)
|
||||
if (dimensionsSeparatorPos == -1)
|
||||
{
|
||||
var imageWidthStr = markdown.Substring(imageDimensionsPos + 2, pos - imageDimensionsPos - 2);
|
||||
|
||||
|
|
|
@ -354,10 +354,10 @@ namespace Microsoft.Toolkit.Parsers.Markdown.Inlines
|
|||
continue;
|
||||
}
|
||||
|
||||
// Okay, we have an entity, but is it one we recognise?
|
||||
// Okay, we have an entity, but is it one we recognize?
|
||||
string entityName = markdown.Substring(sequenceStartIndex + 1, semicolonIndex - (sequenceStartIndex + 1));
|
||||
|
||||
// Unrecognised entity.
|
||||
// Unrecognized entity.
|
||||
if (_entities.ContainsKey(entityName) == false)
|
||||
{
|
||||
continue;
|
||||
|
|
|
@ -101,7 +101,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown
|
|||
var paragraphText = new StringBuilder();
|
||||
|
||||
// These are needed to parse underline-style header blocks.
|
||||
int previousRealtStartOfLine = start;
|
||||
int previousRealStartOfLine = start;
|
||||
int previousStartOfLine = start;
|
||||
int previousEndOfLine = start;
|
||||
|
||||
|
@ -160,18 +160,18 @@ namespace Microsoft.Toolkit.Parsers.Markdown
|
|||
else
|
||||
{
|
||||
int lastIndentation = 0;
|
||||
string lastline = null;
|
||||
string lastLine = null;
|
||||
|
||||
// Determines how many Quote levels were in the last line.
|
||||
if (realStartOfLine > 0)
|
||||
{
|
||||
lastline = markdown.Substring(previousRealtStartOfLine, previousEndOfLine - previousRealtStartOfLine);
|
||||
lastIndentation = lastline.Count(c => c == '>');
|
||||
lastLine = markdown.Substring(previousRealStartOfLine, previousEndOfLine - previousRealStartOfLine);
|
||||
lastIndentation = lastLine.Count(c => c == '>');
|
||||
}
|
||||
|
||||
var currentEndOfLine = Common.FindNextSingleNewLine(markdown, nonSpacePos, end, out _);
|
||||
var currentline = markdown.Substring(realStartOfLine, currentEndOfLine - realStartOfLine);
|
||||
var currentIndentation = currentline.Count(c => c == '>');
|
||||
var currentLine = markdown.Substring(realStartOfLine, currentEndOfLine - realStartOfLine);
|
||||
var currentIndentation = currentLine.Count(c => c == '>');
|
||||
var firstChar = markdown[realStartOfLine];
|
||||
|
||||
// This is a quote that doesn't start with a Quote marker, but carries on from the last line.
|
||||
|
@ -364,7 +364,7 @@ namespace Microsoft.Toolkit.Parsers.Markdown
|
|||
}
|
||||
|
||||
// Repeat.
|
||||
previousRealtStartOfLine = realStartOfLine;
|
||||
previousRealStartOfLine = realStartOfLine;
|
||||
previousStartOfLine = startOfLine;
|
||||
previousEndOfLine = endOfLine;
|
||||
startOfLine = startOfNextLine;
|
||||
|
|
|
@ -8,10 +8,8 @@
|
|||
|
||||
Markdown: Allows you to parse a Markdown String into a Markdown Document, and then Render it with a Markdown Renderer.
|
||||
|
||||
RSS: Allows you to parse an RSS content String into an RSS Schema.
|
||||
|
||||
</Description>
|
||||
<PackageTags>UWP Toolkit Windows Parsers Parsing Markdown RSS</PackageTags>
|
||||
<PackageTags>UWP Toolkit Windows Parsers Parsing Markdown</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Toolkit.Extensions;
|
||||
|
||||
namespace Microsoft.Toolkit.Parsers.Rss
|
||||
{
|
||||
/// <summary>
|
||||
/// Parser for Atom endpoints.
|
||||
/// </summary>
|
||||
internal class AtomParser : BaseRssParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Atom reader implementation to parse Atom content.
|
||||
/// </summary>
|
||||
/// <param name="doc">XDocument to parse.</param>
|
||||
/// <returns>Strong typed response.</returns>
|
||||
public override IEnumerable<RssSchema> LoadFeed(XDocument doc)
|
||||
{
|
||||
Collection<RssSchema> feed = new Collection<RssSchema>();
|
||||
|
||||
if (doc.Root == null)
|
||||
{
|
||||
return feed;
|
||||
}
|
||||
|
||||
var items = doc.Root.Elements(doc.Root.GetDefaultNamespace() + "entry").Select(item => GetRssSchema(item)).ToList<RssSchema>();
|
||||
|
||||
feed = new Collection<RssSchema>(items);
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retieves strong type for passed item.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement to parse.</param>
|
||||
/// <returns>Strong typed object.</returns>
|
||||
private static RssSchema GetRssSchema(XElement item)
|
||||
{
|
||||
RssSchema rssItem = new RssSchema
|
||||
{
|
||||
Author = GetItemAuthor(item),
|
||||
Title = item.GetSafeElementString("title").Trim().DecodeHtml(),
|
||||
ImageUrl = GetItemImage(item),
|
||||
PublishDate = item.GetSafeElementDate("published"),
|
||||
FeedUrl = item.GetLink("alternate"),
|
||||
};
|
||||
|
||||
var content = GetItemContent(item);
|
||||
|
||||
// Removes scripts from html
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
rssItem.Summary = ProcessHtmlSummary(content);
|
||||
rssItem.Content = ProcessHtmlContent(content);
|
||||
}
|
||||
|
||||
string id = item.GetSafeElementString("guid").Trim();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = item.GetSafeElementString("id").Trim();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = rssItem.FeedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
rssItem.InternalID = id;
|
||||
return rssItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves item author from XElement.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item.</param>
|
||||
/// <returns>String of Item Author.</returns>
|
||||
private static string GetItemAuthor(XElement item)
|
||||
{
|
||||
var content = string.Empty;
|
||||
|
||||
if (item != null && item.Element(item.GetDefaultNamespace() + "author") != null)
|
||||
{
|
||||
content = item.Element(item.GetDefaultNamespace() + "author").GetSafeElementString("name");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = item.GetSafeElementString("author");
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns item image from XElement item.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item.</param>
|
||||
/// <returns>String pointing to item image.</returns>
|
||||
private static string GetItemImage(XElement item)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.GetSafeElementString("image")))
|
||||
{
|
||||
return item.GetSafeElementString("image");
|
||||
}
|
||||
|
||||
return item.GetImage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns item content from XElement item.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item.</param>
|
||||
/// <returns>String of item content.</returns>
|
||||
private static string GetItemContent(XElement item)
|
||||
{
|
||||
var content = item.GetSafeElementString("description");
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = item.GetSafeElementString("content");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = item.GetSafeElementString("summary");
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Toolkit.Extensions;
|
||||
|
||||
namespace Microsoft.Toolkit.Parsers.Rss
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for Rss Parser(s).
|
||||
/// </summary>
|
||||
internal abstract class BaseRssParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve feed type from XDocument.
|
||||
/// </summary>
|
||||
/// <param name="doc">XDocument doc.</param>
|
||||
/// <returns>Return feed type.</returns>
|
||||
public static RssType GetFeedType(XDocument doc)
|
||||
{
|
||||
if (doc.Root == null)
|
||||
{
|
||||
return RssType.Unknown;
|
||||
}
|
||||
|
||||
XNamespace defaultNamespace = doc.Root.GetDefaultNamespace();
|
||||
return defaultNamespace.NamespaceName.EndsWith("Atom") ? RssType.Atom : RssType.Rss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstract method to be override by specific implementations of the reader.
|
||||
/// </summary>
|
||||
/// <param name="doc">XDocument doc.</param>
|
||||
/// <returns>Returns list of strongly typed results.</returns>
|
||||
public abstract IEnumerable<RssSchema> LoadFeed(XDocument doc);
|
||||
|
||||
/// <summary>
|
||||
/// Fix up the HTML content.
|
||||
/// </summary>
|
||||
/// <param name="htmlContent">Content to be fixed up.</param>
|
||||
/// <returns>Fixed up content.</returns>
|
||||
protected internal static string ProcessHtmlContent(string htmlContent)
|
||||
{
|
||||
return htmlContent.FixHtml().SanitizeString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a summary of the HTML content.
|
||||
/// </summary>
|
||||
/// <param name="htmlContent">Content to be processed.</param>
|
||||
/// <returns>Summary of the content.</returns>
|
||||
protected internal static string ProcessHtmlSummary(string htmlContent)
|
||||
{
|
||||
return htmlContent.DecodeHtml().Trim().Truncate(500).SanitizeString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.Toolkit.Parsers.Rss
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of Rss.
|
||||
/// </summary>
|
||||
internal enum RssType
|
||||
{
|
||||
/// <summary>
|
||||
/// Atom
|
||||
/// </summary>
|
||||
Atom,
|
||||
|
||||
/// <summary>
|
||||
/// RSS
|
||||
/// </summary>
|
||||
Rss,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Toolkit.Extensions;
|
||||
|
||||
namespace Microsoft.Toolkit.Parsers.Rss
|
||||
{
|
||||
/// <summary>
|
||||
/// Rss reader implementation to parse Rss content.
|
||||
/// </summary>
|
||||
internal class Rss2Parser : BaseRssParser
|
||||
{
|
||||
/// <summary>
|
||||
/// RDF Namespace Uri.
|
||||
/// </summary>
|
||||
private static readonly XNamespace NsRdfNamespaceUri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
||||
|
||||
/// <summary>
|
||||
/// RDF Elements Namespace Uri.
|
||||
/// </summary>
|
||||
private static readonly XNamespace NsRdfElementsNamespaceUri = "http://purl.org/dc/elements/1.1/";
|
||||
|
||||
/// <summary>
|
||||
/// RDF Content Namespace Uri.
|
||||
/// </summary>
|
||||
private static readonly XNamespace NsRdfContentNamespaceUri = "http://purl.org/rss/1.0/modules/content/";
|
||||
|
||||
/// <summary>
|
||||
/// This override load and parses the document and return a list of RssSchema values.
|
||||
/// </summary>
|
||||
/// <param name="doc">XDocument to be loaded.</param>
|
||||
/// <returns>Strongly typed list of feeds.</returns>
|
||||
public override IEnumerable<RssSchema> LoadFeed(XDocument doc)
|
||||
{
|
||||
bool isRDF = false;
|
||||
var feed = new Collection<RssSchema>();
|
||||
XNamespace defaultNamespace = string.Empty;
|
||||
|
||||
if (doc.Root != null)
|
||||
{
|
||||
isRDF = doc.Root.Name == (NsRdfNamespaceUri + "RDF");
|
||||
defaultNamespace = doc.Root.GetDefaultNamespace();
|
||||
}
|
||||
|
||||
foreach (var item in doc.Descendants(defaultNamespace + "item"))
|
||||
{
|
||||
var rssItem = isRDF ? ParseRDFItem(item) : ParseRssItem(item);
|
||||
feed.Add(rssItem);
|
||||
}
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses XElement item into strong typed object.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item to parse.</param>
|
||||
/// <returns>Strong typed object.</returns>
|
||||
private static RssSchema ParseItem(XElement item)
|
||||
{
|
||||
var rssItem = new RssSchema();
|
||||
rssItem.Title = item.GetSafeElementString("title").Trim().DecodeHtml();
|
||||
rssItem.FeedUrl = item.GetSafeElementString("link");
|
||||
|
||||
rssItem.Author = GetItemAuthor(item);
|
||||
|
||||
string content = item.GetSafeElementString("encoded", NsRdfContentNamespaceUri);
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = item.GetSafeElementString("description");
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = item.GetSafeElementString("content");
|
||||
}
|
||||
}
|
||||
|
||||
var summary = item.GetSafeElementString("description");
|
||||
if (string.IsNullOrEmpty(summary))
|
||||
{
|
||||
summary = item.GetSafeElementString("encoded", NsRdfContentNamespaceUri);
|
||||
}
|
||||
|
||||
// Removes scripts from html
|
||||
if (!string.IsNullOrEmpty(summary))
|
||||
{
|
||||
rssItem.Summary = ProcessHtmlSummary(summary);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
rssItem.Content = ProcessHtmlContent(content);
|
||||
}
|
||||
|
||||
string id = item.GetSafeElementString("guid").Trim();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = item.GetSafeElementString("id").Trim();
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = rssItem.FeedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
rssItem.InternalID = id;
|
||||
|
||||
return rssItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses RSS version 1.0 objects.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item.</param>
|
||||
/// <returns>Strong typed object.</returns>
|
||||
private static RssSchema ParseRDFItem(XElement item)
|
||||
{
|
||||
XNamespace ns = "http://search.yahoo.com/mrss/";
|
||||
var rssItem = ParseItem(item);
|
||||
|
||||
rssItem.PublishDate = item.GetSafeElementDate("date", NsRdfElementsNamespaceUri);
|
||||
|
||||
string image = item.GetSafeElementString("image");
|
||||
if (string.IsNullOrEmpty(image) && item.Elements(ns + "thumbnail").LastOrDefault() != null)
|
||||
{
|
||||
var element = item.Elements(ns + "thumbnail").Last();
|
||||
image = element.Attribute("url").Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(image) && item.ToString().Contains("thumbnail"))
|
||||
{
|
||||
image = item.GetSafeElementString("thumbnail");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(image))
|
||||
{
|
||||
image = item.GetImage();
|
||||
}
|
||||
|
||||
rssItem.ImageUrl = image;
|
||||
|
||||
return rssItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses RSS version 2.0 objects.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item.</param>
|
||||
/// <returns>Strong typed object.</returns>
|
||||
private static RssSchema ParseRssItem(XElement item)
|
||||
{
|
||||
XNamespace ns = "http://search.yahoo.com/mrss/";
|
||||
var rssItem = ParseItem(item);
|
||||
|
||||
rssItem.PublishDate = item.GetSafeElementDate("pubDate");
|
||||
|
||||
string image = item.GetSafeElementString("image");
|
||||
if (string.IsNullOrEmpty(image))
|
||||
{
|
||||
image = item.GetImageFromEnclosure();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(image) && item.Elements(ns + "content").LastOrDefault() != null)
|
||||
{
|
||||
var element = item.Elements(ns + "content").Last();
|
||||
if (element.Attribute("type") != null && element.Attribute("type").Value.Contains("image/"))
|
||||
{
|
||||
image = element.Attribute("url").Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(image) && item.Elements(ns + "thumbnail").LastOrDefault() != null)
|
||||
{
|
||||
var element = item.Elements(ns + "thumbnail").Last();
|
||||
image = element.Attribute("url").Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(image) && item.ToString().Contains("thumbnail"))
|
||||
{
|
||||
image = item.GetSafeElementString("thumbnail");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(image))
|
||||
{
|
||||
image = item.GetImage();
|
||||
}
|
||||
|
||||
rssItem.Categories = item.GetSafeElementsString("category");
|
||||
|
||||
rssItem.ImageUrl = image;
|
||||
|
||||
return rssItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve item author from item.
|
||||
/// </summary>
|
||||
/// <param name="item">XElement item.</param>
|
||||
/// <returns>String of item author.</returns>
|
||||
private static string GetItemAuthor(XElement item)
|
||||
{
|
||||
var content = item.GetSafeElementString("creator", NsRdfElementsNamespaceUri).Trim();
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = item.GetSafeElementString("author");
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче