Merge pull request #917 from unoplatform/dev/dr/corePresentation
feat(reactive): Allow generation of VM on non UI assembly
This commit is contained in:
Коммит
9e77ddf64d
|
@ -18,4 +18,8 @@
|
|||
<!--This override is used to validate the use of specific version of the C# Compiler-->
|
||||
<PackageReference Include="Microsoft.Net.Compilers.Toolset" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Reference generators props. This required only when referencing extensions project as source code (instead of packages) -->
|
||||
<Import Project="$(MSBuildThisFileDirectory)\..\..\src\Uno.Extensions.Core.Generators\buildTransitive\Uno.Extensions.Core.props" />
|
||||
<Import Project="$(MSBuildThisFileDirectory)\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
</Project>
|
||||
|
|
|
@ -182,7 +182,6 @@
|
|||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
<Import Project="..\Playground.Shared\Playground.Shared.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
<Target Name="Issue3897Workaround" Condition=" '$(ManagedDesignTimeBuild)' == 'True' " AfterTargets="_RemoveLegacyDesigner">
|
||||
|
|
|
@ -46,8 +46,6 @@
|
|||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage.UI\Uno.Extensions.Storage.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage\Uno.Extensions.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
|
||||
<Import Project="..\Playground.Shared\Playground.Shared.projitems" Label="Shared" />
|
||||
</Project>
|
|
@ -39,8 +39,6 @@
|
|||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage.UI\Uno.Extensions.Storage.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage\Uno.Extensions.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
|
||||
<Import Project="..\Playground.Shared\Playground.Shared.projitems" Label="Shared" />
|
||||
</Project>
|
|
@ -241,8 +241,6 @@
|
|||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
|
|
@ -76,8 +76,6 @@
|
|||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage.UI\Uno.Extensions.Storage.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage\Uno.Extensions.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
|
||||
<Import Project="..\Playground.Shared\Playground.Shared.projitems" Label="Shared" Condition="Exists('..\Playground.Shared\Playground.Shared.projitems')" />
|
||||
</Project>
|
|
@ -107,8 +107,6 @@
|
|||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage.UI\Uno.Extensions.Storage.WinUI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Uno.Extensions.Storage\Uno.Extensions.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
<Import Project="..\Playground.Shared\Playground.Shared.projitems" Label="Shared" />
|
||||
|
||||
<Target Name="WorkaroundSkiaWasmTargets" AfterTargets="ResolveProjectReferences">
|
||||
|
|
|
@ -259,7 +259,6 @@
|
|||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
<Import Project="..\Playground.Shared\Playground.Shared.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
||||
</Project>
|
|
@ -194,7 +194,6 @@
|
|||
|
||||
</ItemGroup>
|
||||
<!-- Reference to Reactive.props only required when referencing Reactive as source code -->
|
||||
<Import Project="..\..\..\src\Uno.Extensions.Reactive.Generator\buildTransitive\Uno.Extensions.Reactive.props" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Mac\Xamarin.Mac.CSharp.targets" />
|
||||
<Target Name="VS16Mac_RemoveSystemMemory" BeforeTargets="ResolveAssemblyReferences">
|
||||
<!--
|
||||
|
|
|
@ -171,10 +171,4 @@
|
|||
|
||||
<Import Project="Uno.CrossTargeting.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="$(AssemblyName).UI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).WinUI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||
<InternalsVisibleTo Include="TestHarnessApp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -42,7 +42,9 @@
|
|||
<PackageVersion Include="Uno.Core" Version="4.0.1" />
|
||||
<PackageVersion Include="Uno.Extensions.Logging.OSLog" Version="1.3.0" />
|
||||
<PackageVersion Include="Uno.Extensions.Logging.WebAssembly.Console" Version="1.3.0" />
|
||||
<PackageVersion Include="Uno.Extensions.Markup.Generators" Version="1.0.0-dev.125" />
|
||||
<PackageVersion Include="Uno.Roslyn" Version="1.3.0-dev.12" />
|
||||
<PackageVersion Include="Uno.Toolkit" Version="2.4.0-dev.114" />
|
||||
<PackageVersion Include="Uno.Toolkit.UI" Version="2.4.0-dev.114" />
|
||||
<PackageVersion Include="Uno.Toolkit.WinUI" Version="2.4.0-dev.114" />
|
||||
<PackageVersion Include="Uno.UI" Version="4.6.19" />
|
||||
|
@ -50,12 +52,11 @@
|
|||
<PackageVersion Include="Uno.UI.MSAL" Version="4.6.19" />
|
||||
<PackageVersion Include="Uno.UI.Runtime.WebAssembly" Version="4.6.19" />
|
||||
<PackageVersion Include="Uno.WinUI" Version="4.6.19" />
|
||||
<PackageVersion Include="Uno.WinUI.Markup" Version="4.6.0-dev.12" />
|
||||
<PackageVersion Include="Uno.WinUI.MSAL" Version="4.6.19" />
|
||||
<PackageVersion Include="Uno.WinUI.Runtime.WebAssembly" Version="4.6.19" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
|
||||
<PackageVersion Include="xunit" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.1" />
|
||||
<PackageVersion Include="Uno.WinUI.Markup" Version="4.6.0-dev.12" />
|
||||
<PackageVersion Include="Uno.Extensions.Markup.Generators" Version="1.0.0-dev.125" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
<PackageReference Include="System.Threading.Tasks.Extensions" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="$(AssemblyName).UI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).WinUI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Msal.UI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Msal.WinUI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Oidc.UI" />
|
||||
|
|
|
@ -15,8 +15,12 @@ namespace Uno.Extensions.Generators;
|
|||
|
||||
internal static class RoslynExtensions
|
||||
{
|
||||
private static readonly SymbolDisplayFormat _fullStringFormat = SymbolDisplayFormat
|
||||
.FullyQualifiedFormat
|
||||
.WithParameterOptions(SymbolDisplayParameterOptions.IncludeName | SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeDefaultValue | SymbolDisplayParameterOptions.IncludeParamsRefOut);
|
||||
|
||||
public static string ToFullString(this ISymbol symbol)
|
||||
=> symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
=> symbol.ToDisplayString(_fullStringFormat);
|
||||
|
||||
public static IEnumerable<INamedTypeSymbol> GetNamespaceTypes(this INamespaceSymbol sym)
|
||||
{
|
||||
|
|
|
@ -24,16 +24,38 @@ internal static class CodeGenToolExtensions
|
|||
#pragma warning disable".Align(Math.Max(aligned - 1, 0));
|
||||
|
||||
public static string AsPartialOf(this ICodeGenTool tool, INamedTypeSymbol type, string code)
|
||||
=> AsPartialOf(tool, type, null, code);
|
||||
=> AsPartialOf(tool, type, null, null, code);
|
||||
|
||||
public static string AsPartialOf(this ICodeGenTool tool, INamedTypeSymbol type, string? bases, string code)
|
||||
public static string AsPartialOf(this ICodeGenTool tool, INamedTypeSymbol type, string? attributes, string? bases, string code)
|
||||
{
|
||||
var types = type
|
||||
.GetContainingTypes()
|
||||
.Reverse()
|
||||
.Select(t => $"partial {t.ToDisplayString(_symbolDeclaration)}")
|
||||
.ToList();
|
||||
types.Add($"{attributes?.Align(0)}\r\npartial {type.ToDisplayString(_symbolDeclaration)}{(bases is null ? "" : $" : {bases}")}");
|
||||
|
||||
return $@"{tool.GetFileHeader(3)}
|
||||
|
||||
using global::System;
|
||||
using global::System.Linq;
|
||||
using global::System.Threading.Tasks;
|
||||
|
||||
namespace {type.ContainingNamespace}
|
||||
{{
|
||||
{types.Select((def, i) => $"{def.Indent(i + (i == 0 ? 0 : 4))}\r\n{"{".Indent(i + 4)}").JoinBy("\r\n")}
|
||||
{code.Align(4 + types.Count)}
|
||||
{types.Select((_, i) => "}".Indent((types.Count - 1 - i) + (i == 0 ? 0 : 4))).JoinBy("\r\n")}
|
||||
}}".Align(0);
|
||||
}
|
||||
|
||||
public static string InSameNamespaceOf(this ICodeGenTool tool, INamedTypeSymbol type, string code)
|
||||
{
|
||||
var types = type
|
||||
.GetContainingTypes()
|
||||
.Reverse()
|
||||
.Select(t => $"partial {t.ToDisplayString(_symbolDeclaration)}")
|
||||
.ToList();
|
||||
types.Add($"partial {type.ToDisplayString(_symbolDeclaration)}{(bases is null ? "" : $" : {bases}")}");
|
||||
|
||||
return $@"{tool.GetFileHeader(3)}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Generators;
|
||||
|
||||
internal record struct GeneratedFile(string Name, string Code);
|
|
@ -209,8 +209,9 @@ internal class KeyEqualityGenerationTool : ICodeGenTool
|
|||
|
||||
return this.AsPartialOf(
|
||||
type,
|
||||
$"{NS.Equality}.IKeyEquatable<{type}>{(config.IKeyed is null ? "" : ", " + config.IKeyed.ToFullString())}", // i.e. config.IKeyEquatable
|
||||
$@"
|
||||
attributes: null,
|
||||
bases: $"{NS.Equality}.IKeyEquatable<{type}>{(config.IKeyed is null ? "" : ", " + config.IKeyed.ToFullString())}", // i.e. config.IKeyEquatable
|
||||
code: $@"
|
||||
{(config.IsCustomImplementation
|
||||
? "// Skipping IKeyed.Key as user is providing a custom implementation of IKeyEquatable"
|
||||
: $@"/// <inheritdoc cref=""{{NS.Equality}}.IKeyed{{T}}"" />
|
||||
|
|
|
@ -35,9 +35,10 @@ internal class PropertySelectorsGenerationTool : ICodeGenTool
|
|||
return;
|
||||
}
|
||||
|
||||
var assembly = candidate.Context.SemanticModel.Compilation.Assembly;
|
||||
var id = GetId(candidate);
|
||||
|
||||
ctx.AddSource(id, GenerateRegistrationClass(id, usage.Value, accessors));
|
||||
|
||||
ctx.AddSource(id, GenerateRegistrationClass(assembly, id, usage.Value, accessors));
|
||||
}
|
||||
catch (GenerationException genError)
|
||||
{
|
||||
|
@ -78,10 +79,10 @@ internal class PropertySelectorsGenerationTool : ICodeGenTool
|
|||
}
|
||||
}
|
||||
|
||||
private string GenerateRegistrationClass(string id, PropertySelectorUsage usage, IEnumerable<(string key, string accessor)> accessors)
|
||||
private string GenerateRegistrationClass(IAssemblySymbol assembly, string id, PropertySelectorUsage usage, IEnumerable<(string key, string accessor)> accessors)
|
||||
=> $@"{this.GetFileHeader(3)}
|
||||
|
||||
namespace {NS.EditionNonGlobal}.__PropertySelectors
|
||||
namespace {assembly.Name}.__PropertySelectors
|
||||
{{
|
||||
/// <summary>
|
||||
/// Auto registration class for PropertySelector used in {usage.Method.ContainingModule.GlobalNamespace}.
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Configuration")]
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Navigation")]
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Navigation.UI")]
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Navigation.WinUI")]
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Navigation.Toolkit.UI")]
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Navigation.Toolkit.WinUI")]
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Equality;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.ComponentModel;
|
||||
global using System.Diagnostics;
|
||||
global using System.Linq;
|
||||
global using System.Runtime.CompilerServices;
|
||||
|
@ -10,4 +11,3 @@ global using Microsoft.Extensions.DependencyInjection;
|
|||
global using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using Uno.Extensions.DependencyInjection;
|
||||
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
namespace Uno.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// An abstraction of the dispatcher that is associated to the UI thread.
|
||||
/// </summary>
|
||||
public interface IDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously executes an operation on the UI thread.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">Type of the result of the operation.</typeparam>
|
||||
/// <param name="func">The async operation to execute.</param>
|
||||
/// <param name="cancellation">An cancellation token to cancel the async operation.</param>
|
||||
/// <returns>A ValueTask to asynchronously get the result of the operation.</returns>
|
||||
ValueTask<TResult> ExecuteAsync<TResult>(AsyncFunc<TResult> func, CancellationToken cancellation);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that specifies whether the current execution context is on the UI thread.
|
||||
/// </summary>
|
||||
bool HasThreadAccess { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to the queue which will be executed on the thread associated with the dispatcher.
|
||||
/// </summary>
|
||||
/// <remarks>This is the raw version which allows to interact with the native dispatcher the fewest overhead possible.</remarks>
|
||||
/// <param name="action">The task to execute.</param>
|
||||
/// <returns>True indicates that the task was added to the queue; false, otherwise.</returns>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] // Applications should prefer to use the ExecuteAsync which allow to track the execution
|
||||
bool TryEnqueue(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously executes an operation on the UI thread.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">Type of the result of the operation.</typeparam>
|
||||
/// <param name="action">The async operation to execute.</param>
|
||||
/// <param name="cancellation">An cancellation token to cancel the async operation.</param>
|
||||
/// <returns>A ValueTask to asynchronously get the result of the operation.</returns>
|
||||
ValueTask<TResult> ExecuteAsync<TResult>(AsyncFunc<TResult> action, CancellationToken cancellation);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Uno.Extensions.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// An re-entrant asynchronous lock, that can be used in conjunction with C# async/await
|
||||
/// </summary>
|
||||
public sealed class FastAsyncLock
|
||||
{
|
||||
private readonly AsyncLocal<AsyncLocalMonitor> _localMonitor = new AsyncLocal<AsyncLocalMonitor>();
|
||||
|
||||
private AsyncLocalMonitor? _tail;
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the lock, then provides a disposable to release it.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token to cancel the acquisition of the lock</param>
|
||||
/// <returns>An IDisposable instance that allows the release of the lock.</returns>
|
||||
public Task<IDisposable> LockAsync(CancellationToken ct)
|
||||
{
|
||||
var monitor = _localMonitor.Value;
|
||||
if (monitor?.TryReEnterSync() ?? false)
|
||||
{
|
||||
// If the node from the async local is not null, it means that the current 'ExecutionContext' already acquired the lock.
|
||||
// Check if we can re-enter (if the lock was not released yet), and if so continue with current monitor.
|
||||
return Task.FromResult<IDisposable>(new Handle(monitor));
|
||||
}
|
||||
|
||||
// Creates a new monitor and set it on current 'ExecutionContext' to allow re-entrency
|
||||
_localMonitor.Value = monitor = new AsyncLocalMonitor(this);
|
||||
|
||||
// Then enqueue this new monitor
|
||||
var previous = Interlocked.Exchange(ref _tail, monitor);
|
||||
if (previous?.TryEnqueue(monitor) ?? false)
|
||||
{
|
||||
// The lock was already aquired by someone else, add us into the queue
|
||||
return monitor.EnterAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The waiting queue was empty (or the previous node is already completed)
|
||||
// Tt means that we sucessfully acquired the lock
|
||||
return Task.FromResult(monitor.EnterSync());
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncLocalMonitor
|
||||
{
|
||||
private readonly object _exitGate = new object();
|
||||
private readonly FastAsyncLock _owner;
|
||||
|
||||
private int _state = State.Waiting;
|
||||
private TaskCompletionSource<IDisposable>? _enterAsync;
|
||||
private int _count;
|
||||
private AsyncLocalMonitor? _next;
|
||||
|
||||
private static class State
|
||||
{
|
||||
public const int Waiting = 0;
|
||||
public const int Entered = 1;
|
||||
public const int Exited = 2;
|
||||
public const int Aborted = int.MaxValue;
|
||||
}
|
||||
|
||||
public AsyncLocalMonitor(FastAsyncLock owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public IDisposable EnterSync()
|
||||
{
|
||||
// No concurrency consideration: we are on a single 'ExecutionContext' (i.e. on a single thread at a time)
|
||||
|
||||
Debug.Assert(_count == 0);
|
||||
Debug.Assert(_state == State.Waiting);
|
||||
|
||||
_count = 1;
|
||||
_state = State.Entered;
|
||||
|
||||
return new Handle(this);
|
||||
}
|
||||
|
||||
public bool TryReEnterSync()
|
||||
{
|
||||
// No concurrency consideration: we are on a single 'ExecutionContext' (i.e. on a single thread at a time)
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case State.Waiting:
|
||||
throw new InvalidOperationException("'ExecutionContext' corrupted.");
|
||||
|
||||
case State.Entered:
|
||||
Interlocked.Increment(ref _count);
|
||||
lock (_exitGate)
|
||||
{
|
||||
return _state == State.Entered;
|
||||
}
|
||||
|
||||
case State.Aborted:
|
||||
case State.Exited:
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IDisposable> EnterAsync(CancellationToken ct)
|
||||
{
|
||||
// The item may have been already dequeued by the previous since it was set as '_next',
|
||||
// If so, return sync (note: the '_count' was already incremented)
|
||||
if (_state == State.Waiting)
|
||||
{
|
||||
_enterAsync = new TaskCompletionSource<IDisposable>();
|
||||
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
_enterAsync.Task.GetAwaiter().OnCompleted(ct.Register(Abort).Dispose);
|
||||
|
||||
void Abort()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _state, State.Aborted, State.Waiting) == State.Waiting)
|
||||
{
|
||||
_enterAsync.SetCanceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This instance may have been dequeued by previous while we where initiazing the async hanlde,
|
||||
// so make sure to not wait a task that will never complete.
|
||||
if (_state == State.Waiting)
|
||||
{
|
||||
return _enterAsync.Task;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IDisposable>(new Handle(this));
|
||||
}
|
||||
|
||||
private void Dequeued()
|
||||
{
|
||||
// Dequeing may occures more than once. So move to next step only if item is really waiting!
|
||||
switch (Interlocked.CompareExchange(ref _state, State.Entered, State.Waiting))
|
||||
{
|
||||
case State.Waiting:
|
||||
_count = 1;
|
||||
_enterAsync?.SetResult(new Handle(this)); // null check: item may be dequeued even if EnterAsync was not yet invoked
|
||||
break;
|
||||
|
||||
case State.Aborted:
|
||||
_next?.Dequeued();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryEnqueue(AsyncLocalMonitor next)
|
||||
{
|
||||
Debug.Assert(_next == null);
|
||||
|
||||
if (_state >= State.Exited)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// First set item as '_next'
|
||||
_next = next;
|
||||
if (_state >= State.Exited)
|
||||
{
|
||||
// It may occures that this monitor is being exited while we where setting '_next'.
|
||||
// If so, make sure that the '_next' is being dequeued.
|
||||
// Note: This may conduct the item to be 'Dequeued' twice (if it was already set while exiting, '_next' has already been dequeued)
|
||||
|
||||
_next.Dequeued();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
// As we are running asynchronous, it may occures that the inner task completes from another execution context (e.g. an event).
|
||||
// So unlike the synchronous lock which must be exited from the same execution context / thread (otherwise we receive an SynchronizationLockException),
|
||||
// here we cannot enforce the 'Exit' to be invoked only from the entering execution context (ie. check that '_owner._localMonitor.Value == this')
|
||||
// Consequently, we have to handle concurrency with ReEntrency (Enter{Sync|Async} are not impacted since for those the caller did not received an 'Handle' yet)
|
||||
|
||||
if (Interlocked.Decrement(ref _count) == 0)
|
||||
{
|
||||
lock (_exitGate)
|
||||
{
|
||||
if (_count == 0 && Interlocked.CompareExchange(ref _state, State.Exited, State.Entered) == State.Entered)
|
||||
{
|
||||
_next?.Dequeued();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An handle on an async lock which makes sure that disposing it mutiple times won't exit the monitor multiple times
|
||||
/// </summary>
|
||||
private class Handle : IDisposable
|
||||
{
|
||||
private readonly AsyncLocalMonitor _awaiter;
|
||||
private int _isDisposed;
|
||||
|
||||
public Handle(AsyncLocalMonitor awaiter)
|
||||
{
|
||||
_awaiter = awaiter;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
|
||||
{
|
||||
_awaiter.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,9 +66,6 @@
|
|||
<InternalsVisibleTo Include="Uno.Extensions.Storage" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Storage.UI" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Storage.WinUI" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Reactive.UI" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Reactive.WinUI" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Reactive.Tests" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Configuration" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Navigation" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Navigation.UI" />
|
||||
|
|
|
@ -1,241 +0,0 @@
|
|||
// ******************************************************************
|
||||
// Copyright <20> 2015-2018 nventive inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// ******************************************************************
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Uno.Extensions.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// An re-entrant asynchronous lock, that can be used in conjuction with C# async/await
|
||||
/// </summary>
|
||||
internal sealed class FastAsyncLock
|
||||
{
|
||||
private readonly AsyncLocal<AsyncLocalMonitor> _localMonitor = new AsyncLocal<AsyncLocalMonitor>();
|
||||
|
||||
private AsyncLocalMonitor? _tail;
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the lock, then provides a disposable to release it.
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token to cancel the acquisition of the lock</param>
|
||||
/// <returns>An IDisposable instance that allows the release of the lock.</returns>
|
||||
public Task<IDisposable> LockAsync(CancellationToken ct)
|
||||
{
|
||||
var monitor = _localMonitor.Value;
|
||||
if (monitor?.TryReEnterSync() ?? false)
|
||||
{
|
||||
// If the node from the async local is not null, it means that the current 'ExecutionContext' already acquired the lock.
|
||||
// Check if we can re-enter (if the lock was not released yet), and if so continue with current monitor.
|
||||
return Task.FromResult<IDisposable>(new Handle(monitor));
|
||||
}
|
||||
|
||||
// Creates a new monitor and set it on current 'ExecutionContext' to allow re-entrency
|
||||
_localMonitor.Value = monitor = new AsyncLocalMonitor(this);
|
||||
|
||||
// Then enqueue this new monitor
|
||||
var previous = Interlocked.Exchange(ref _tail, monitor);
|
||||
if (previous?.TryEnqueue(monitor) ?? false)
|
||||
{
|
||||
// The lock was already aquired by someone else, add us into the queue
|
||||
return monitor.EnterAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The waiting queue was empty (or the previous node is already completed)
|
||||
// Tt means that we sucessfully acquired the lock
|
||||
return Task.FromResult(monitor.EnterSync());
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncLocalMonitor
|
||||
{
|
||||
private readonly object _exitGate = new object();
|
||||
private readonly FastAsyncLock _owner;
|
||||
|
||||
private int _state = State.Waiting;
|
||||
private TaskCompletionSource<IDisposable>? _enterAsync;
|
||||
private int _count;
|
||||
private AsyncLocalMonitor? _next;
|
||||
|
||||
private static class State
|
||||
{
|
||||
public const int Waiting = 0;
|
||||
public const int Entered = 1;
|
||||
public const int Exited = 2;
|
||||
public const int Aborted = int.MaxValue;
|
||||
}
|
||||
|
||||
public AsyncLocalMonitor(FastAsyncLock owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public IDisposable EnterSync()
|
||||
{
|
||||
// No concurrency consideration: we are on a single 'ExecutionContext' (i.e. on a single thread at a time)
|
||||
|
||||
Debug.Assert(_count == 0);
|
||||
Debug.Assert(_state == State.Waiting);
|
||||
|
||||
_count = 1;
|
||||
_state = State.Entered;
|
||||
|
||||
return new Handle(this);
|
||||
}
|
||||
|
||||
public bool TryReEnterSync()
|
||||
{
|
||||
// No concurrency consideration: we are on a single 'ExecutionContext' (i.e. on a single thread at a time)
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case State.Waiting:
|
||||
throw new InvalidOperationException("'ExecutionContext' corrupted.");
|
||||
|
||||
case State.Entered:
|
||||
Interlocked.Increment(ref _count);
|
||||
lock (_exitGate)
|
||||
{
|
||||
return _state == State.Entered;
|
||||
}
|
||||
|
||||
case State.Aborted:
|
||||
case State.Exited:
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IDisposable> EnterAsync(CancellationToken ct)
|
||||
{
|
||||
// The item may have been already dequeued by the previous since it was set as '_next',
|
||||
// If so, return sync (note: the '_count' was already incremented)
|
||||
if (_state == State.Waiting)
|
||||
{
|
||||
_enterAsync = new TaskCompletionSource<IDisposable>();
|
||||
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
_enterAsync.Task.GetAwaiter().OnCompleted(ct.Register(Abort).Dispose);
|
||||
|
||||
void Abort()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _state, State.Aborted, State.Waiting) == State.Waiting)
|
||||
{
|
||||
_enterAsync.SetCanceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This instance may have been dequeued by previous while we where initiazing the async hanlde,
|
||||
// so make sure to not wait a task that will never complete.
|
||||
if (_state == State.Waiting)
|
||||
{
|
||||
return _enterAsync.Task;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IDisposable>(new Handle(this));
|
||||
}
|
||||
|
||||
private void Dequeued()
|
||||
{
|
||||
// Dequeing may occures more than once. So move to next step only if item is really waiting!
|
||||
switch (Interlocked.CompareExchange(ref _state, State.Entered, State.Waiting))
|
||||
{
|
||||
case State.Waiting:
|
||||
_count = 1;
|
||||
_enterAsync?.SetResult(new Handle(this)); // null check: item may be dequeued even if EnterAsync was not yet invoked
|
||||
break;
|
||||
|
||||
case State.Aborted:
|
||||
_next?.Dequeued();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryEnqueue(AsyncLocalMonitor next)
|
||||
{
|
||||
Debug.Assert(_next == null);
|
||||
|
||||
if (_state >= State.Exited)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// First set item as '_next'
|
||||
_next = next;
|
||||
if (_state >= State.Exited)
|
||||
{
|
||||
// It may occures that this monitor is being exited while we where setting '_next'.
|
||||
// If so, make sure that the '_next' is being dequeued.
|
||||
// Note: This may conduct the item to be 'Dequeued' twice (if it was already set while exiting, '_next' has already been dequeued)
|
||||
|
||||
_next.Dequeued();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
// As we are running asynchronous, it may occures that the inner task completes from another execution context (e.g. an event).
|
||||
// So unlike the synchronous lock which must be exited from the same execution context / thread (otherwise we receive an SynchronizationLockException),
|
||||
// here we cannot enforce the 'Exit' to be invoked only from the entering execution context (ie. check that '_owner._localMonitor.Value == this')
|
||||
// Consequently, we have to handle concurrency with ReEntrency (Enter{Sync|Async} are not impacted since for those the caller did not received an 'Handle' yet)
|
||||
|
||||
if (Interlocked.Decrement(ref _count) == 0)
|
||||
{
|
||||
lock (_exitGate)
|
||||
{
|
||||
if (_count == 0 && Interlocked.CompareExchange(ref _state, State.Exited, State.Entered) == State.Entered)
|
||||
{
|
||||
_next?.Dequeued();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An handle on an async lock which makes sure that disposing it mutiple times won't exit the monitor multiple times
|
||||
/// </summary>
|
||||
private class Handle : IDisposable
|
||||
{
|
||||
private readonly AsyncLocalMonitor _awaiter;
|
||||
private int _isDisposed;
|
||||
|
||||
public Handle(AsyncLocalMonitor awaiter)
|
||||
{
|
||||
_awaiter = awaiter;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
|
||||
{
|
||||
_awaiter.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,5 +30,4 @@
|
|||
<InternalsVisibleTo Include="Uno.Extensions.Authentication" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
using Windows.UI.Xaml;
|
||||
|
||||
namespace Uno.Extensions;
|
||||
namespace Uno.Extensions;
|
||||
|
||||
public class Dispatcher : IDispatcher
|
||||
{
|
||||
|
@ -31,6 +29,17 @@ public class Dispatcher : IDispatcher
|
|||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryEnqueue(Action action)
|
||||
#if WINUI
|
||||
=> _dispatcher.TryEnqueue(() => action());
|
||||
#else
|
||||
{
|
||||
_ = _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action());
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TResult> ExecuteAsync<TResult>(AsyncFunc<TResult> func, CancellationToken cancellation)
|
||||
{
|
||||
|
|
|
@ -31,7 +31,8 @@ global using Uno.Extensions.Navigation.UI.Controls;
|
|||
global using Microsoft.UI.Xaml.Data;
|
||||
global using Microsoft.UI.Xaml.Media;
|
||||
#else
|
||||
global using Windows.System;
|
||||
global using Windows.System;
|
||||
global using Windows.UI.Core;
|
||||
global using Windows.UI.Xaml;
|
||||
global using Windows.UI.Xaml.Controls;
|
||||
global using Windows.UI.Xaml.Controls.Primitives;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Toolkit.UI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Toolkit.WinUI" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Authentication" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Authentication.UI" />
|
||||
<InternalsVisibleTo Include="Uno.Extensions.Authentication.WinUI" />
|
||||
|
|
|
@ -23,5 +23,6 @@ internal record BindableListFromFeedOfListField(IFieldSymbol Field, ITypeSymbol
|
|||
=> @$"
|
||||
var {Field.GetCamelCaseName()}Source = {N.Ctor.Model}.{Field.Name} ?? throw new NullReferenceException(""The list feed field '{Field.Name}' is null. Public feeds properties must be initialized in the constructor."");
|
||||
var {Field.GetCamelCaseName()}SourceListFeed = {N.ListFeed.Extensions.ToListFeed}<{CollectionType}, {ItemType}>({Field.GetCamelCaseName()}Source);
|
||||
{Field.Name} = new {NS.Bindings}.BindableListFeed<{ItemType}>(nameof({Field.Name}), {Field.GetCamelCaseName()}SourceListFeed, {N.Ctor.Ctx});";
|
||||
var {Field.GetCamelCaseName()}SourceListState = {N.Ctor.Ctx}.GetOrCreateListState({Field.GetCamelCaseName()}SourceListFeed);
|
||||
{Field.Name} = {NS.Bindings}.BindableHelper.CreateBindableList(nameof({Field.Name}), {Field.GetCamelCaseName()}SourceListState);";
|
||||
}
|
||||
|
|
|
@ -23,5 +23,6 @@ internal record BindableListFromFeedOfListProperty(IPropertySymbol Property, ITy
|
|||
=> @$"
|
||||
var {Property.GetCamelCaseName()}Source = {N.Ctor.Model}.{Property.Name} ?? throw new NullReferenceException(""The list feed property '{Property.Name}' is null. Public feeds properties must be initialized in the constructor."");
|
||||
var {Property.GetCamelCaseName()}SourceListFeed = {N.ListFeed.Extensions.ToListFeed}<{CollectionType}, {ItemType}>({Property.GetCamelCaseName()}Source);
|
||||
{Property.Name} = new {NS.Bindings}.BindableListFeed<{ItemType}>(nameof({Property.Name}), {Property.GetCamelCaseName()}SourceListFeed, {N.Ctor.Ctx});";
|
||||
var {Property.GetCamelCaseName()}SourceListState = {N.Ctor.Ctx}.GetOrCreateListState({Property.GetCamelCaseName()}SourceListFeed);
|
||||
{Property.Name} = {NS.Bindings}.BindableHelper.CreateBindableList(nameof({Property.Name}), {Property.GetCamelCaseName()}SourceListState);";
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ internal record BindableListFromListFeedField(IFieldSymbol _field, ITypeSymbol _
|
|||
|
||||
/// <inheritdoc />
|
||||
public string? GetInitialization()
|
||||
=> @$"{_field.Name} = new {NS.Bindings}.BindableListFeed<{_valueType}>(
|
||||
nameof({_field.Name}),
|
||||
{N.Ctor.Model}.{_field.Name} ?? throw new NullReferenceException(""The list feed field '{_field.Name}' is null. Public feeds properties must be initialized in the constructor.""),
|
||||
{N.Ctor.Ctx});";
|
||||
=> @$"
|
||||
var {_field.GetCamelCaseName()}Source = {N.Ctor.Model}.{_field.Name} ?? throw new NullReferenceException(""The list feed field '{_field.Name}' is null. Public feeds properties must be initialized in the constructor."");
|
||||
var {_field.GetCamelCaseName()}SourceListState = {N.Ctor.Ctx}.GetOrCreateListState({_field.GetCamelCaseName()}Source);
|
||||
{_field.Name} = {NS.Bindings}.BindableHelper.CreateBindableList(nameof({_field.Name}), {_field.GetCamelCaseName()}SourceListState);";
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ internal record BindableListFromListFeedProperty(IPropertySymbol _property, ITyp
|
|||
|
||||
/// <inheritdoc />
|
||||
public string GetInitialization()
|
||||
=> @$"{_property.Name} = new {NS.Bindings}.BindableListFeed<{_valueType}>(
|
||||
nameof({_property.Name}),
|
||||
{N.Ctor.Model}.{_property.Name} ?? throw new NullReferenceException(""The list feed property '{_property.Name}' is null. Public feeds properties must be initialized in the constructor.""),
|
||||
{N.Ctor.Ctx});";
|
||||
=> @$"
|
||||
var {_property.GetCamelCaseName()}Source = {N.Ctor.Model}.{_property.Name} ?? throw new NullReferenceException(""The list feed property '{_property.Name}' is null. Public feeds properties must be initialized in the constructor."");
|
||||
var {_property.GetCamelCaseName()}SourceListState = {N.Ctor.Ctx}.GetOrCreateListState({_property.GetCamelCaseName()}Source);
|
||||
{_property.Name} = {NS.Bindings}.BindableHelper.CreateBindableList(nameof({_property.Name}), {_property.GetCamelCaseName()}SourceListState);";
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ using static Microsoft.CodeAnalysis.Accessibility;
|
|||
|
||||
namespace Uno.Extensions.Reactive.Generator;
|
||||
|
||||
internal class BindableViewModelGenerator : ICodeGenTool
|
||||
internal class ViewModelGenTool_1 : ICodeGenTool
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Version => "1";
|
||||
|
@ -22,7 +22,7 @@ internal class BindableViewModelGenerator : ICodeGenTool
|
|||
private readonly BindableViewModelMappingGenerator _viewModelsMapping;
|
||||
private readonly IAssemblySymbol _assembly;
|
||||
|
||||
public BindableViewModelGenerator(BindableGenerationContext ctx)
|
||||
public ViewModelGenTool_1(BindableGenerationContext ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_bindables = new BindableGenerator(ctx);
|
||||
|
@ -31,27 +31,9 @@ internal class BindableViewModelGenerator : ICodeGenTool
|
|||
}
|
||||
|
||||
private bool IsSupported(INamedTypeSymbol? type)
|
||||
{
|
||||
if (type is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ctx.IsGenerationEnabled(type) is {} isEnabled)
|
||||
{
|
||||
// If the attribute is set, we don't check for the `partial`: the build as to fail if not
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
if (type.IsPartial()
|
||||
&& (type.ContainingAssembly.FindAttribute<ImplicitBindablesAttribute>() ?? new ()) is { IsEnabled: true } @implicit // Note: the type might be from another assembly than current
|
||||
&& @implicit.Patterns.Any(pattern => Regex.IsMatch(type.ToString(), pattern)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
=> type is not null
|
||||
&& (_ctx.IsGenerationEnabled(type)
|
||||
?? type.Name.EndsWith("ViewModel", StringComparison.Ordinal) && type.IsPartial());
|
||||
|
||||
public IEnumerable<(string fileName, string code)> Generate()
|
||||
{
|
||||
|
@ -74,18 +56,7 @@ internal class BindableViewModelGenerator : ICodeGenTool
|
|||
}
|
||||
|
||||
private static string GetViewModelName(INamedTypeSymbol type)
|
||||
{
|
||||
// Note: the type might be from another assembly than current
|
||||
var isLegacyMode = type.ContainingAssembly.FindAttribute<ImplicitBindablesAttribute>() is { Patterns.Length: 1 } config
|
||||
&& config.Patterns[0] is ImplicitBindablesAttribute.LegacyPattern;
|
||||
|
||||
return isLegacyMode switch
|
||||
{
|
||||
true => $"Bindable{type.Name}",
|
||||
_ when type.Name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase) => $"Bindable{type.Name}",
|
||||
_ => $"{type.Name.TrimEnd("Model", StringComparison.OrdinalIgnoreCase)}ViewModel",
|
||||
};
|
||||
}
|
||||
=> $"Bindable{type.Name}";
|
||||
|
||||
private string Generate(INamedTypeSymbol model)
|
||||
{
|
||||
|
@ -215,8 +186,9 @@ internal class BindableViewModelGenerator : ICodeGenTool
|
|||
|
||||
var fileCode = this.AsPartialOf(
|
||||
model,
|
||||
$"global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware",
|
||||
$@"
|
||||
attributes: null,
|
||||
bases: $"global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware",
|
||||
code: $@"
|
||||
{bindableVmCode.Align(4)}
|
||||
|
||||
/// <inheritdoc />
|
|
@ -0,0 +1,232 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Uno.Extensions.Generators;
|
||||
using Uno.Extensions.Reactive.Config;
|
||||
using Uno.RoslynHelpers;
|
||||
using static Microsoft.CodeAnalysis.Accessibility;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Generator;
|
||||
|
||||
internal class ViewModelGenTool_2 : ICodeGenTool
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Version => "2";
|
||||
|
||||
private readonly BindableGenerationContext _ctx;
|
||||
private readonly BindableGenerator _bindables;
|
||||
private readonly BindableViewModelMappingGenerator _viewModelsMapping;
|
||||
private readonly IAssemblySymbol _assembly;
|
||||
|
||||
public ViewModelGenTool_2(BindableGenerationContext ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_bindables = new BindableGenerator(ctx);
|
||||
_viewModelsMapping = new BindableViewModelMappingGenerator(ctx);
|
||||
_assembly = ctx.Context.Compilation.Assembly;
|
||||
}
|
||||
|
||||
private bool IsSupported([NotNullWhen(true)] INamedTypeSymbol? type)
|
||||
{
|
||||
if (type is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ctx.IsGenerationEnabled(type) is {} isEnabled)
|
||||
{
|
||||
// If the attribute is set, we don't check for the `partial`: the build as to fail if not
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
if (type.IsPartial()
|
||||
&& (type.ContainingAssembly.FindAttribute<ImplicitBindablesAttribute>() ?? new ()) is { IsEnabled: true } @implicit // Note: the type might be from another assembly than current
|
||||
&& @implicit.Patterns.Any(pattern => Regex.IsMatch(type.ToString(), pattern)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<(string fileName, string code)> Generate()
|
||||
{
|
||||
var models = from module in _assembly.Modules
|
||||
from type in module.GetNamespaceTypes()
|
||||
where IsSupported(type)
|
||||
select type;
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
yield return (model + ".Bindable", GenerateViewModel(model));
|
||||
yield return (model.ToString(), GeneratePartialModel(model));
|
||||
}
|
||||
|
||||
foreach (var (type, code) in _bindables.Generate())
|
||||
{
|
||||
yield return (type.ToString(), code: code);
|
||||
}
|
||||
|
||||
yield return _viewModelsMapping.Generate();
|
||||
}
|
||||
|
||||
private static string GetViewModelName(INamedTypeSymbol model)
|
||||
=> $"Bindable{model.Name}";
|
||||
|
||||
private static string GetViewModelFullName(INamedTypeSymbol model)
|
||||
=> $"{model.ToFullString().TrimEnd(model.Name, StringComparison.Ordinal)}Bindable{model.Name}";
|
||||
|
||||
private string GenerateViewModel(INamedTypeSymbol model)
|
||||
{
|
||||
var vmName = GetViewModelName(model);
|
||||
var hasBaseType = IsSupported(model.BaseType);
|
||||
var baseType = hasBaseType
|
||||
? GetViewModelFullName(model.BaseType!)
|
||||
: $"{NS.Bindings}.BindableViewModelBase";
|
||||
|
||||
var members = GetMembers(model).ToList();
|
||||
var vm = this.InSameNamespaceOf(
|
||||
model,
|
||||
$@"
|
||||
{this.GetCodeGenAttribute()}
|
||||
[{NS.Bindings}.ViewModel(typeof({model.ToFullString()}))]
|
||||
{model.DeclaredAccessibility.ToCSharpCodeString()} partial class {vmName} : {baseType}
|
||||
{{
|
||||
{members.Select(member => member.GetBackingField()).Align(5)}
|
||||
|
||||
{model
|
||||
.Constructors
|
||||
.Where(ctor => !ctor.IsCloneCtor(model)
|
||||
// we do not support inheritance of ctor, inheritance always goes through the same BindableVM(vm) ctor.
|
||||
&& ctor.DeclaredAccessibility is not Protected and not ProtectedAndFriend and not ProtectedAndInternal and not Private)
|
||||
.Where(_ctx.IsGenerationNotDisable)
|
||||
.Select(ctor => $@"
|
||||
{GetCtorAccessibility(ctor)} {vmName}({ctor.Parameters.Select(p => p.ToFullString()).JoinBy(", ")})
|
||||
: this(new {model}({ctor.Parameters.Select(p => p.Name).JoinBy(", ")}))
|
||||
{{
|
||||
}}")
|
||||
.Align(5)}
|
||||
|
||||
protected {vmName}({model} {N.Ctor.Model}){(hasBaseType ? $" : base({N.Ctor.Model})" : "")}
|
||||
{{
|
||||
var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model});
|
||||
|
||||
{(hasBaseType ? "" : @$"// Share the context between Model and ViewModel
|
||||
{NS.Core}.SourceContext.Set(this, {N.Ctor.Ctx});
|
||||
base.RegisterDisposable({N.Ctor.Model});
|
||||
{N.Model} = {N.Ctor.Model};").Align(6)}
|
||||
|
||||
{N.Ctor.Model}.__reactiveBindableViewModel = this;
|
||||
|
||||
{members.Select(member => member.GetInitialization()).Align(6)}
|
||||
}}
|
||||
|
||||
public {(hasBaseType ? $"new {model} {N.Model} => ({model}) base.{N.Model};" : $"{model} {N.Model} {{ get; }}")}
|
||||
|
||||
{members.Select(member => member.GetDeclaration()).Align(5)}
|
||||
}}");
|
||||
|
||||
|
||||
// If type is at least internally accessible, add it to a mapping from the VM type to it's bindable counterpart to ease usage in navigation engine.
|
||||
// (Private types are almost only a test case which is not supported by nav anyway)
|
||||
if (model.DeclaredAccessibility is not Accessibility.Private
|
||||
&& model.GetContainingTypes().All(type => type.DeclaredAccessibility is not Accessibility.Private))
|
||||
{
|
||||
_viewModelsMapping.Register(model, GetViewModelFullName(model));
|
||||
}
|
||||
|
||||
return vm.Align(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of the
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
private string GeneratePartialModel(INamedTypeSymbol model)
|
||||
{
|
||||
var vm = GetViewModelFullName(model);
|
||||
return this.AsPartialOf(
|
||||
model,
|
||||
attributes: $"[{NS.Bindings}.Model(typeof({vm}))]",
|
||||
bases: $"global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware, {NS.Bindings}.IModel<{vm}>",
|
||||
code: $@"
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
internal {vm} __reactiveBindableViewModel = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
{vm} {NS.Bindings}.IModel<{vm}>.ViewModel => __reactiveBindableViewModel;
|
||||
|
||||
/// <inheritdoc />
|
||||
{this.GetCodeGenAttribute()}
|
||||
public global::System.Threading.Tasks.ValueTask DisposeAsync()
|
||||
=> {NS.Core}.SourceContext.Find(this)?.DisposeAsync() ?? default;
|
||||
");
|
||||
}
|
||||
|
||||
private string GetCtorAccessibility(IMethodSymbol ctor)
|
||||
=> ctor.DeclaredAccessibility == Accessibility.Private
|
||||
? "public"
|
||||
: ctor.GetAccessibilityAsCSharpCodeString();
|
||||
|
||||
private IEnumerable<IMappedMember> GetMembers(INamedTypeSymbol type)
|
||||
{
|
||||
foreach (var member in type.GetMembers().Where(member => member.IsAccessible() && !member.IsStatic))
|
||||
{
|
||||
switch (member)
|
||||
{
|
||||
case IFieldSymbol field when _ctx.IsListFeed(field.Type, out var valueType):
|
||||
yield return new BindableListFromListFeedField(field, valueType);
|
||||
break;
|
||||
|
||||
case IFieldSymbol field when _ctx.IsFeedOfList(field.Type, out var collectionType, out var valueType):
|
||||
yield return new BindableListFromFeedOfListField(field, collectionType, valueType);
|
||||
break;
|
||||
|
||||
case IFieldSymbol field when _ctx.IsFeed(field.Type, out var valueType):
|
||||
{
|
||||
yield return _bindables.GetBindableType(valueType) is { } bindableType && !field.HasAttributes(_ctx.ValueAttribute)
|
||||
? new BindableFromFeedField(field, valueType, bindableType)
|
||||
: new PropertyFromFeedField(field, valueType);
|
||||
break;
|
||||
}
|
||||
|
||||
case IFieldSymbol field:
|
||||
yield return new MappedField(field);
|
||||
break;
|
||||
|
||||
case IPropertySymbol property when _ctx.IsListFeed(property.Type, out var valueType):
|
||||
yield return new BindableListFromListFeedProperty(property, valueType);
|
||||
break;
|
||||
|
||||
case IPropertySymbol property when _ctx.IsFeedOfList(property.Type, out var collectionType, out var valueType):
|
||||
yield return new BindableListFromFeedOfListProperty(property, collectionType, valueType);
|
||||
break;
|
||||
|
||||
case IPropertySymbol property when _ctx.IsFeed(property.Type, out var valueType):
|
||||
{
|
||||
yield return _bindables.GetBindableType(valueType) is { } bindableType && !property.HasAttributes(_ctx.ValueAttribute)
|
||||
? new BindableFromFeedProperty(property, valueType, bindableType)
|
||||
: new PropertyFromFeedProperty(property, valueType);
|
||||
break;
|
||||
}
|
||||
|
||||
case IPropertySymbol property:
|
||||
yield return new MappedProperty(property);
|
||||
break;
|
||||
|
||||
case IMethodSymbol method when CommandFromMethod.TryCreate(type, method, _ctx, out var commandGenerator):
|
||||
yield return commandGenerator;
|
||||
break;
|
||||
|
||||
case IMethodSymbol { MethodKind: MethodKind.Ordinary, IsImplicitlyDeclared: false } method:
|
||||
yield return new MappedMethod(method);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Uno.Extensions.Generators;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Generator.Dispatching;
|
||||
|
||||
internal record UIModuleInitializerGenerationContext(
|
||||
GeneratorExecutionContext Context,
|
||||
[ContextType("Uno.Extensions.Reactive.UI.ModuleInitializer?")] INamedTypeSymbol? ReactiveUIModuleInitializer);
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Uno.Extensions.Generators;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Generator.Dispatching;
|
||||
|
||||
internal class UIModuleInitializerGenerationTool : ICodeGenTool
|
||||
{
|
||||
private readonly UIModuleInitializerGenerationContext _ctx;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Version => "1";
|
||||
|
||||
public UIModuleInitializerGenerationTool(UIModuleInitializerGenerationContext context)
|
||||
{
|
||||
_ctx = context;
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<GeneratedFile> Generate()
|
||||
{
|
||||
if (_ctx.ReactiveUIModuleInitializer is not null)
|
||||
{
|
||||
var assembly = _ctx.Context.Compilation.Assembly.Name;
|
||||
yield return new(
|
||||
$"{assembly}.ReactiveUIModuleInitializer",
|
||||
@$"{this.GetFileHeader(4)}
|
||||
|
||||
namespace {assembly}
|
||||
{{
|
||||
/// <summary>
|
||||
/// Initialize provider of dispatcher.
|
||||
/// </summary>
|
||||
/// <remarks>This class ensures that dispatcher has been initialized even if the Reactive.UI package has not been loaded yet.</remarks>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
{this.GetCodeGenAttribute()}
|
||||
internal static class __ReactiveUIModuleInitializer
|
||||
{{
|
||||
/// <summary>
|
||||
/// Register the <seealso cref=""DispatcherQueueProvider""/> as provider of <see cref=""IDispatcher""/> for the reactive platform.
|
||||
/// </summary>
|
||||
/// <remarks>This method is flagged with ModuleInitializer attribute and should not be used by application.</remarks>
|
||||
[global::System.Runtime.CompilerServices.ModuleInitializer]
|
||||
public static void Initialize()
|
||||
{{
|
||||
global::Uno.Extensions.Reactive.UI.ModuleInitializer.Initialize();
|
||||
}}
|
||||
}}
|
||||
}}".Align(0));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,11 +3,12 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Uno.Extensions.Generators;
|
||||
using Uno.Extensions.Reactive.Config;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Generator;
|
||||
|
||||
/// <summary>
|
||||
/// A generator that generates bindable VM for the feed framework
|
||||
/// A generator that generates bindable VM for the reactive framework
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public partial class FeedsGenerator : ISourceGenerator
|
||||
|
@ -29,9 +30,22 @@ public partial class FeedsGenerator : ISourceGenerator
|
|||
|
||||
if (GenerationContext.TryGet<BindableGenerationContext>(context, out var error) is {} bindableContext)
|
||||
{
|
||||
foreach (var generated in new BindableViewModelGenerator(bindableContext).Generate())
|
||||
var tool = bindableContext.Context.Compilation.Assembly.FindAttribute<BindableGenerationToolAttribute>() ?? new BindableGenerationToolAttribute();
|
||||
switch (tool.Version)
|
||||
{
|
||||
context.AddSource(PathHelper.SanitizeFileName(generated.fileName) + ".g.cs", generated.code);
|
||||
case 1:
|
||||
foreach (var generated in new ViewModelGenTool_1(bindableContext).Generate())
|
||||
{
|
||||
context.AddSource(PathHelper.SanitizeFileName(generated.fileName) + ".g.cs", generated.code);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
foreach (var generated in new ViewModelGenTool_2(bindableContext).Generate())
|
||||
{
|
||||
context.AddSource(PathHelper.SanitizeFileName(generated.fileName) + ".g.cs", generated.code);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Uno.Extensions.Generators;
|
||||
using Uno.Extensions.Reactive.Config;
|
||||
using Uno.Extensions.Reactive.Generator.Dispatching;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Generator;
|
||||
|
||||
/// <summary>
|
||||
/// A generator that generates UI module initialization for the reactive framework.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public class UIModuleInitializerGenerator : ISourceGenerator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Initialize(GeneratorInitializationContext context) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
#if DEBUGGING_GENERATOR
|
||||
var process = Process.GetCurrentProcess().ProcessName;
|
||||
if (process.IndexOf("VBCSCompiler", StringComparison.OrdinalIgnoreCase) is not -1
|
||||
|| process.IndexOf("csc", StringComparison.OrdinalIgnoreCase) is not -1)
|
||||
{
|
||||
Debugger.Launch();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GenerationContext.TryGet<UIModuleInitializerGenerationContext>(context, out _) is { } dispatcherContext)
|
||||
{
|
||||
foreach (var generated in new UIModuleInitializerGenerationTool(dispatcherContext).Generate())
|
||||
{
|
||||
context.AddSource(PathHelper.SanitizeFileName(generated.Name) + ".g.cs", generated.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|||
|
||||
namespace Uno.Extensions.Reactive.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Base class to tests class that are using the reactive framework.
|
||||
/// </summary>
|
||||
public class FeedTests
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -27,6 +30,9 @@ public class FeedTests
|
|||
/// </summary>
|
||||
public TestContext TestContext { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// A global cancellation token which will be cancelled on tear down.
|
||||
/// </summary>
|
||||
public CancellationToken CT => Context?.SourceContext.Token ?? TestContext?.CancellationTokenSource.Token ?? CancellationToken.None;
|
||||
|
||||
/// <summary>
|
||||
|
@ -34,10 +40,16 @@ public class FeedTests
|
|||
/// </summary>
|
||||
protected FeedTestContext Context { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the test context.
|
||||
/// </summary>
|
||||
[TestInitialize]
|
||||
public virtual void Initialize()
|
||||
=> Context = TestInitialize(TestContext);
|
||||
|
||||
/// <summary>
|
||||
/// Tear down the test context.
|
||||
/// </summary>
|
||||
[TestCleanup]
|
||||
public virtual void Cleanup()
|
||||
=> TestCleanup(Context);
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IDispatcher"/> that can be used to abstract the UI thread in tests.
|
||||
/// </summary>
|
||||
public sealed class TestDispatcher : IDispatcher, IDisposable
|
||||
{
|
||||
private readonly Thread _thread;
|
||||
private readonly Queue<Action> _queue = new();
|
||||
private readonly AutoResetEvent _evt = new(false);
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance.
|
||||
/// </summary>
|
||||
public TestDispatcher()
|
||||
{
|
||||
_thread = new Thread(Run);
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasThreadAccess => Thread.CurrentThread == _thread;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryEnqueue(Action action)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
// For tests we prefer to throw instead of returning false in order to make clear the invalid usage.
|
||||
throw new InvalidOperationException("Dispatcher has already been aborted!");
|
||||
}
|
||||
|
||||
lock (_queue)
|
||||
{
|
||||
_queue.Enqueue(action);
|
||||
}
|
||||
|
||||
_evt.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TResult> ExecuteAsync<TResult>(AsyncFunc<TResult> action, CancellationToken ct)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<TResult>();
|
||||
using var ctReg = ct.CanBeCanceled ? ct.Register(() => tcs.TrySetCanceled()) : default;
|
||||
|
||||
TryEnqueue(Execute);
|
||||
|
||||
return await tcs.Task;
|
||||
|
||||
async void Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.TrySetResult(await action(ct));
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
tcs.TrySetException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
while (!_isDisposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool hasItem;
|
||||
Action? item;
|
||||
lock (_queue)
|
||||
{
|
||||
hasItem = _queue.Count > 0;
|
||||
item = hasItem ? _queue.Dequeue() : default;
|
||||
}
|
||||
|
||||
if (hasItem)
|
||||
{
|
||||
item!();
|
||||
}
|
||||
else
|
||||
{
|
||||
_evt.WaitOne();
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
throw new InvalidOperationException("Got an exception on the UI thread", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_isDisposed = true;
|
||||
_evt.Set();
|
||||
_thread.Join();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
@ -10,6 +11,8 @@ using FluentAssertions;
|
|||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Uno.Extensions.Equality;
|
||||
using Uno.Extensions.Reactive.Commands;
|
||||
using Uno.Extensions.Reactive.Core;
|
||||
using Uno.Extensions.Reactive.Dispatching;
|
||||
using Uno.Extensions.Reactive.Testing;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Tests.Commands;
|
||||
|
@ -125,6 +128,27 @@ public class Given_AsyncCommand : FeedUITests
|
|||
await executions[0].Wait();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task When_ProvidingParameter_Then_ParameterNotInvokedOnUIThread()
|
||||
{
|
||||
var isOnUIThread = new TaskCompletionSource<bool>();
|
||||
async IAsyncEnumerable<IMessage> Parameter(SourceContext ctx)
|
||||
{
|
||||
isOnUIThread.TrySetResult(DispatcherHelper.HasThreadAccess);
|
||||
yield break;
|
||||
}
|
||||
var config = new CommandConfig
|
||||
{
|
||||
Parameter = Parameter,
|
||||
Execute = async (p, ct) => { }
|
||||
};
|
||||
var (sut, executions) = Create(config);
|
||||
|
||||
await ExecuteOnDispatcher(() => sut.Execute(null));
|
||||
|
||||
(await isOnUIThread.Task).Should().BeFalse();
|
||||
}
|
||||
|
||||
private async Task WaitFor(Func<bool> predicate)
|
||||
{
|
||||
for (var i = 0; i < 100; i++)
|
||||
|
|
|
@ -18,176 +18,21 @@ public partial class Given_BasicViewModel_Then_Generate : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task Test_Constructors()
|
||||
{
|
||||
await using var bindableCtor1 = new Given_BasicViewModel_Then_Generate__ViewModel.BindableGiven_BasicViewModel_Then_Generate__ViewModel();
|
||||
await using var bindableCtor1 = new BindableGiven_BasicViewModel_Then_Generate__ViewModel();
|
||||
|
||||
await using var bindableCtor2 = new Given_BasicViewModel_Then_Generate__ViewModel.BindableGiven_BasicViewModel_Then_Generate__ViewModel(aRandomService: "aRandomService");
|
||||
await using var bindableCtor2 = new BindableGiven_BasicViewModel_Then_Generate__ViewModel(aRandomService: "aRandomService");
|
||||
|
||||
await using var bindableCtor3 = new Given_BasicViewModel_Then_Generate__ViewModel.BindableGiven_BasicViewModel_Then_Generate__ViewModel(
|
||||
await using var bindableCtor3 = new BindableGiven_BasicViewModel_Then_Generate__ViewModel(
|
||||
anExternalInput: default(IFeed<string>)!,
|
||||
anExternalReadWriteInput: default(IState<string>)!,
|
||||
anExternalRecordInput: default(IFeed<MyRecord>)!,
|
||||
anExternalWeirdRecordInput: default(IFeed<MyWeirdRecord>)!);
|
||||
|
||||
await using var bindableCtor4 = new Given_BasicViewModel_Then_Generate__ViewModel.BindableGiven_BasicViewModel_Then_Generate__ViewModel(
|
||||
aParameterToNotBeAParameterLessCtor1: (short)0,
|
||||
defaultAnInput: default(string)!,
|
||||
defaultAReadWriteInput: default(string)!,
|
||||
defaultARecordInput: default(MyRecord)!,
|
||||
defaultAWeirdRecordInput: default(MyWeirdRecord)!,
|
||||
defaultARecordWithAValuePropertyInput: default(MyRecordWithAValueProperty)!,
|
||||
defaultAnInputConflictingWithAProperty: (int)42);
|
||||
|
||||
await using var bindableCtor5 = new Given_BasicViewModel_Then_Generate__ViewModel.BindableGiven_BasicViewModel_Then_Generate__ViewModel(
|
||||
aParameterToNotBeAParameterLessCtor2: (int)0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_PublicMembers()
|
||||
{
|
||||
var mysSubRecord = new MySubRecord("prop1", 42);
|
||||
var myWeirdRecord = new MyWeirdRecord();
|
||||
var myRecord = new MyRecord("prop1", 42, mysSubRecord, myWeirdRecord);
|
||||
var myRecordWithAValueProperty = new MyRecordWithAValueProperty("42");
|
||||
|
||||
await using var bindable = new Given_BasicViewModel_Then_Generate__ViewModel.BindableGiven_BasicViewModel_Then_Generate__ViewModel(
|
||||
aParameterToNotBeAParameterLessCtor1: (short)42,
|
||||
defaultAnInput: "anInput",
|
||||
defaultAReadWriteInput: "aReadWriteInput",
|
||||
defaultARecordInput: myRecord,
|
||||
defaultAWeirdRecordInput: myWeirdRecord,
|
||||
defaultARecordWithAValuePropertyInput: myRecordWithAValueProperty,
|
||||
defaultAnInputConflictingWithAProperty: (int)42);
|
||||
|
||||
Assert.IsNotNull(bindable.Model as Given_BasicViewModel_Then_Generate__ViewModel);
|
||||
|
||||
Assert.AreEqual<string>("anInput", bindable.AnInput);
|
||||
bindable.AnInput = "hasSetter";
|
||||
|
||||
Assert.AreEqual<string>("aReadWriteInput", bindable.AReadWriteInput);
|
||||
bindable.AReadWriteInput = "hasSetter";
|
||||
|
||||
Assert.AreEqual<MyRecord>(myRecord, bindable.ARecordInput.GetValue()!);
|
||||
|
||||
// De normalized properties
|
||||
((string)bindable.ARecordInput.Property1).ToString();
|
||||
((int)bindable.ARecordInput.Property2).ToString();
|
||||
((BindableMySubRecord)bindable.ARecordInput.Property3).ToString();
|
||||
((BindableMyWeirdRecord)bindable.ARecordInput.Property4).ToString();
|
||||
((string)bindable.ARecordInput.Property3.Prop1).ToString();
|
||||
((int)bindable.ARecordInput.Property3.Prop2).ToString();
|
||||
((string)bindable.ARecordInput.Property4.ReadWriteProperty).ToString();
|
||||
((string)bindable.ARecordInput.Property4.ReadInitProperty ).ToString();
|
||||
((string)bindable.ARecordInput.Property4.ReadOnlyProperty ).ToString();
|
||||
bindable.ARecordInput.Property4.WriteOnlyProperty = "";
|
||||
bindable.ARecordInput.Property4.InitOnlyProperty = "";
|
||||
((string?)bindable.ARecordInput.Property4.ANullableProperty)?.ToString();
|
||||
|
||||
// Value properties
|
||||
((MySubRecord)bindable.ARecordInput.Property3.Value).ToString();
|
||||
((MyWeirdRecord)bindable.ARecordInput.Property4.Value).ToString();
|
||||
|
||||
((BindableMyWeirdRecord)bindable.AWeirdRecordInput).ToString();
|
||||
((MyWeirdRecord)bindable.AWeirdRecordInput.Value).ToString();
|
||||
|
||||
((BindableMyRecordWithAValueProperty)bindable.ARecordWithAValuePropertyInput).ToString();
|
||||
((string)bindable.ARecordWithAValuePropertyInput.Value).ToString();
|
||||
|
||||
Assert.AreEqual<MyWeirdRecord>(myWeirdRecord, bindable.AWeirdRecordInput.GetValue()!);
|
||||
|
||||
Assert.IsNotNull(bindable.ATriggerInput as IAsyncCommand);
|
||||
|
||||
Assert.IsNotNull(bindable.ATypedTriggerInput as IAsyncCommand);
|
||||
|
||||
bindable.Model.AField = "AField_SetFromVM";
|
||||
Assert.AreEqual("AField_SetFromVM", bindable.AField);
|
||||
bindable.AField = "AField_SetFromBindable";
|
||||
Assert.AreEqual("AField_SetFromBindable", bindable.Model.AField);
|
||||
|
||||
bindable.Model.AnInternalField = "AnInternalField_SetFromVM";
|
||||
Assert.AreEqual("AnInternalField_SetFromVM", bindable.AnInternalField);
|
||||
bindable.AnInternalField = "AnInternalField_SetFromBindable";
|
||||
Assert.AreEqual("AnInternalField_SetFromBindable", bindable.Model.AnInternalField);
|
||||
|
||||
bindable.Model.AProtectedInternalField = "AProtectedInternalField_SetFromVM";
|
||||
Assert.AreEqual("AProtectedInternalField_SetFromVM", bindable.AProtectedInternalField);
|
||||
bindable.AProtectedInternalField = "AProtectedInternalField_SetFromBindable";
|
||||
Assert.AreEqual("AProtectedInternalField_SetFromBindable", bindable.Model.AProtectedInternalField);
|
||||
|
||||
Assert.AreEqual(bindable.Model.AnInputConflictingWithAProperty, "AnInputConflictingWithAProperty");
|
||||
bindable.AnInputConflictingWithAProperty = 42; // This should be of type 'int'
|
||||
|
||||
Assert.IsTrue(bindable.GetType().GetProperty("AFeedField")?.PropertyType == typeof(string));
|
||||
Assert.IsTrue(bindable.GetType().GetProperty("AStateField")?.PropertyType == typeof(string));
|
||||
Assert.IsTrue(bindable.GetType().GetProperty("ACustomFeedField")?.PropertyType == typeof(string));
|
||||
|
||||
Assert.IsNotNull(bindable.ARecordFeedField as IFeed<MyRecord>);
|
||||
Assert.IsNotNull(bindable.ARecordFeedField as BindableMyRecord);
|
||||
Assert.IsFalse(bindable.ARecordFeedField.CanWrite);
|
||||
//Assert.AreEqual(bindable.ARecordFeedField.Property1, "ARecordFeedField"); // Source feed is async
|
||||
|
||||
Assert.IsNotNull(bindable.ARecordStateField as IFeed<MyRecord>);
|
||||
Assert.IsNotNull(bindable.ARecordStateField as BindableMyRecord);
|
||||
Assert.IsTrue(bindable.ARecordStateField.CanWrite);
|
||||
//Assert.AreEqual(bindable.ARecordStateField.Property1, "ARecordStateField"); // Source feed is async
|
||||
|
||||
Assert.IsNotNull(bindable.AListFeedField as IListState<string>);
|
||||
Assert.IsNotNull(bindable.AListFeedField as ICollectionView);
|
||||
|
||||
Assert.IsNotNull(bindable.AListStateField as IListState<string>);
|
||||
Assert.IsNotNull(bindable.AListStateField as ICollectionView);
|
||||
|
||||
bindable.Model.AProperty = "AProperty_SetFromVM";
|
||||
Assert.AreEqual("AProperty_SetFromVM", bindable.AProperty);
|
||||
bindable.AProperty = "AProperty_SetFromBindable";
|
||||
Assert.AreEqual("AProperty_SetFromBindable", bindable.Model.AProperty);
|
||||
|
||||
bindable.Model.AnInternalProperty = "AnInternalProperty_SetFromVM";
|
||||
Assert.AreEqual("AnInternalProperty_SetFromVM", bindable.AnInternalProperty);
|
||||
bindable.AnInternalProperty = "AnInternalProperty_SetFromBindable";
|
||||
Assert.AreEqual("AnInternalProperty_SetFromBindable", bindable.Model.AnInternalProperty);
|
||||
|
||||
bindable.Model.AProtectedInternalProperty = "AProtectedInternalProperty_SetFromVM";
|
||||
Assert.AreEqual("AProtectedInternalProperty_SetFromVM", bindable.AProtectedInternalProperty);
|
||||
bindable.AProtectedInternalProperty = "AProtectedInternalProperty_SetFromBindable";
|
||||
Assert.AreEqual("AProtectedInternalProperty_SetFromBindable", bindable.Model.AProtectedInternalProperty);
|
||||
|
||||
Assert.AreEqual(bindable.Model.AReadOnlyProperty, bindable.AReadOnlyProperty);
|
||||
|
||||
bindable.ASetOnlyProperty = "hasSetter";
|
||||
|
||||
Assert.IsTrue(bindable.GetType().GetProperty("AFeedProperty")?.PropertyType == typeof(string));
|
||||
Assert.IsTrue(bindable.GetType().GetProperty("AStateProperty")?.PropertyType == typeof(string));
|
||||
Assert.IsTrue(bindable.GetType().GetProperty("ACustomFeedProperty")?.PropertyType == typeof(string));
|
||||
|
||||
Assert.IsNotNull(bindable.ARecordFeedProperty as IFeed<MyRecord>);
|
||||
Assert.IsNotNull(bindable.ARecordFeedProperty as BindableMyRecord);
|
||||
Assert.IsFalse(bindable.ARecordFeedProperty.CanWrite);
|
||||
//Assert.AreEqual(bindable.ARecordFeedProperty.Property1, "ARecordFeedProperty"); // Source feed is async
|
||||
|
||||
Assert.IsNotNull(bindable.ARecordStateProperty as IFeed<MyRecord>);
|
||||
Assert.IsNotNull(bindable.ARecordStateProperty as BindableMyRecord);
|
||||
Assert.IsTrue(bindable.ARecordStateProperty.CanWrite);
|
||||
//Assert.AreEqual(bindable.ARecordStateProperty.Property1, "ARecordStateProperty"); // Source feed is async
|
||||
|
||||
Assert.IsNotNull(bindable.AListFeedProperty as IListState<string>);
|
||||
Assert.IsNotNull(bindable.AListFeedProperty as ICollectionView);
|
||||
Assert.IsNotNull(bindable.AListStateProperty as IListState<string>);
|
||||
Assert.IsNotNull(bindable.AListStateProperty as ICollectionView);
|
||||
|
||||
Assert.IsNotNull(bindable.AParameterLessMethod as ICommand);
|
||||
Assert.IsNotNull(bindable.AParameterLessMethodReturningATuple as ICommand);
|
||||
|
||||
bindable.AParameterizedMethod("arg1", 42);
|
||||
|
||||
var (result1, result2) = bindable.AParameterizedMethodReturningATuple("AParameterizedMethodReturningATuple", 43);
|
||||
Assert.AreEqual("AParameterizedMethodReturningATuple", result1);
|
||||
Assert.AreEqual(43, result2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task When_FeedOfKindOfImmutableList_Then_TreatAsListFeed()
|
||||
{
|
||||
await using var bindable = new When_FeedOfKindOfImmutableList_Then_TreatAsListFeed_ViewModel.BindableWhen_FeedOfKindOfImmutableList_Then_TreatAsListFeed_ViewModel();
|
||||
await using var bindable = new BindableWhen_FeedOfKindOfImmutableList_Then_TreatAsListFeed_ViewModel();
|
||||
|
||||
AssertIsValid(bindable.AFeedOfArray);
|
||||
AssertIsValid(bindable.AFeedOfImmutableList);
|
||||
|
@ -204,7 +49,7 @@ public partial class Given_BasicViewModel_Then_Generate : FeedUITests
|
|||
{
|
||||
Assert.IsNotNull(bindable);
|
||||
Assert.IsNotNull(bindable as IListState<string>);
|
||||
Assert.IsNotNull(bindable as ICollectionView);
|
||||
Assert.IsNotNull(bindable as ICollectionView);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,7 +79,7 @@ public partial class Given_BasicViewModel_Then_Generate : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_FeedOfKindOfRawEnumerable_Then_DoNotTreatAsListFeed()
|
||||
{
|
||||
await using var bindable = new When_FeedOfKindOfRawEnumerable_Then_DoNotTreatAsListFeed_ViewModel.BindableWhen_FeedOfKindOfRawEnumerable_Then_DoNotTreatAsListFeed_ViewModel();
|
||||
await using var bindable = new BindableWhen_FeedOfKindOfRawEnumerable_Then_DoNotTreatAsListFeed_ViewModel();
|
||||
|
||||
AssertIsValid(bindable.AFeedOfEnumerable);
|
||||
|
||||
|
|
|
@ -27,32 +27,6 @@ public partial class Given_BasicViewModel_Then_Generate__ViewModel
|
|||
{
|
||||
}
|
||||
|
||||
public Given_BasicViewModel_Then_Generate__ViewModel(
|
||||
short aParameterToNotBeAParameterLessCtor1,
|
||||
IInput<string> anInput,
|
||||
IInput<string> aReadWriteInput,
|
||||
IInput<MyRecord> aRecordInput,
|
||||
IInput<MyWeirdRecord> aWeirdRecordInput,
|
||||
IInput<MyRecordWithAValueProperty> aRecordWithAValuePropertyInput,
|
||||
IInput<int> anInputConflictingWithAProperty)
|
||||
{
|
||||
Assert.IsNotNull(anInput);
|
||||
Assert.IsNotNull(aReadWriteInput);
|
||||
Assert.IsNotNull(aRecordInput);
|
||||
Assert.IsNotNull(aWeirdRecordInput);
|
||||
Assert.IsNotNull(aRecordWithAValuePropertyInput);
|
||||
Assert.IsNotNull(anInputConflictingWithAProperty);
|
||||
}
|
||||
|
||||
public Given_BasicViewModel_Then_Generate__ViewModel(
|
||||
int aParameterToNotBeAParameterLessCtor2,
|
||||
ICommandBuilder aTriggerInput,
|
||||
ICommandBuilder<string> aTypedTriggerInput)
|
||||
{
|
||||
Assert.IsNotNull(aTriggerInput);
|
||||
Assert.IsNotNull(aTypedTriggerInput);
|
||||
}
|
||||
|
||||
public string AnInputConflictingWithAProperty { get; } = "AnInputConflictingWithAProperty";
|
||||
|
||||
public string AField = "AField";
|
||||
|
|
|
@ -30,7 +30,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_ParameterLess_Void()
|
||||
{
|
||||
await using var vm = new When_ParameterLess_Void_ViewModel.BindableWhen_ParameterLess_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_ParameterLess_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute(null);
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -54,7 +54,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneParameter_Void()
|
||||
{
|
||||
await using var vm = new When_OneParameter_Void_ViewModel.BindableWhen_OneParameter_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneParameter_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("42");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -79,7 +79,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneValueTypeParameter_Void()
|
||||
{
|
||||
await using var vm = new When_OneValueTypeParameter_Void_ViewModel.BindableWhen_OneValueTypeParameter_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneValueTypeParameter_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute(new DateTimeOffset(1983, 9, 9, 15, 00, 00, TimeSpan.FromHours(1)));
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -104,7 +104,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneNullableValueTypeParameter_Void()
|
||||
{
|
||||
await using var vm = new When_OneNullableValueTypeParameter_Void_ViewModel.BindableWhen_OneNullableValueTypeParameter_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneNullableValueTypeParameter_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute(new DateTimeOffset(1983, 9, 9, 15, 00, 00, TimeSpan.FromHours(1)));
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -129,7 +129,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneParameterAndCT_Void()
|
||||
{
|
||||
await using var vm = new When_OneParameterAndCT_Void_ViewModel.BindableWhen_OneParameterAndCT_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneParameterAndCT_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("42");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -161,7 +161,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_ParameterLess_Task()
|
||||
{
|
||||
await using var vm = new When_ParameterLess_Task_ViewModel.BindableWhen_ParameterLess_Task_ViewModel();
|
||||
await using var vm = new BindableWhen_ParameterLess_Task_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute(null);
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -201,7 +201,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneParameter_Task()
|
||||
{
|
||||
await using var vm = new When_OneParameter_Task_ViewModel.BindableWhen_OneParameter_Task_ViewModel();
|
||||
await using var vm = new BindableWhen_OneParameter_Task_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("42");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -239,7 +239,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_ParameterLess_ValueTask()
|
||||
{
|
||||
await using var vm = new When_ParameterLess_ValueTask_ViewModel.BindableWhen_ParameterLess_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_ParameterLess_ValueTask_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute(null);
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -279,7 +279,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneParameter_ValueTask()
|
||||
{
|
||||
await using var vm = new When_OneParameter_ValueTask_ViewModel.BindableWhen_OneParameter_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneParameter_ValueTask_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("42");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -320,7 +320,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneParameterAndCT_ValueTask()
|
||||
{
|
||||
await using var vm = new When_OneParameterAndCT_ValueTask_ViewModel.BindableWhen_OneParameterAndCT_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneParameterAndCT_ValueTask_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("42");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -357,7 +357,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameter_Void_WithoutCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameter_Void_ViewModel.BindableWhen_OneFeedParameter_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameter_Void_ViewModel();
|
||||
|
||||
// We have to wait for the external parameter to be provided by the feed
|
||||
await WaitFor(() => vm.MyMethod.CanExecute(null));
|
||||
|
@ -372,7 +372,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameter_Void_WithCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameter_Void_ViewModel.BindableWhen_OneFeedParameter_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameter_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("43");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -403,7 +403,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameterAndCT_Void_WithoutCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameterAndCT_Void_ViewModel.BindableWhen_OneFeedParameterAndCT_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameterAndCT_Void_ViewModel();
|
||||
|
||||
// We have to wait for the external parameter to be provided by the feed
|
||||
await WaitFor(() => vm.MyMethod.CanExecute(null));
|
||||
|
@ -418,7 +418,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameterAndCT_Void_WithCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameterAndCT_Void_ViewModel.BindableWhen_OneFeedParameterAndCT_Void_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameterAndCT_Void_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("43");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -455,7 +455,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameter_ValueTask_WithoutCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameter_ValueTask_ViewModel.BindableWhen_OneFeedParameter_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameter_ValueTask_ViewModel();
|
||||
|
||||
// We have to wait for the external parameter to be provided by the feed
|
||||
await WaitFor(() => vm.MyMethod.CanExecute(null));
|
||||
|
@ -476,7 +476,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameter_ValueTask_WithCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameter_ValueTask_ViewModel.BindableWhen_OneFeedParameter_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameter_ValueTask_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("43");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -519,7 +519,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneListFeedParameter_ValueTask_WithoutCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneListFeedParameter_ValueTask_ViewModel.BindableWhen_OneListFeedParameter_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneListFeedParameter_ValueTask_ViewModel();
|
||||
|
||||
// We have to wait for the external parameter to be provided by the feed
|
||||
await WaitFor(() => vm.MyMethod.CanExecute(null));
|
||||
|
@ -540,7 +540,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneListFeedParameter_ValueTask_WithCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneListFeedParameter_ValueTask_ViewModel.BindableWhen_OneListFeedParameter_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneListFeedParameter_ValueTask_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute(ImmutableList.Create("51", "52", "53"));
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -583,7 +583,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameterAndCT_ValueTask_WithoutCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameterAndCT_ValueTask_ViewModel.BindableWhen_OneFeedParameterAndCT_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameterAndCT_ValueTask_ViewModel();
|
||||
|
||||
// We have to wait for the external parameter to be provided by the feed
|
||||
await WaitFor(() => vm.MyMethod.CanExecute(null));
|
||||
|
@ -604,7 +604,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_OneFeedParameterAndCT_ValueTask_WithCommandParameter()
|
||||
{
|
||||
await using var vm = new When_OneFeedParameterAndCT_ValueTask_ViewModel.BindableWhen_OneFeedParameterAndCT_ValueTask_ViewModel();
|
||||
await using var vm = new BindableWhen_OneFeedParameterAndCT_ValueTask_ViewModel();
|
||||
|
||||
vm.MyMethod.Execute("43");
|
||||
await WaitFor(() => vm.InvokeCount == 1);
|
||||
|
@ -673,7 +673,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[DataRow(nameof(When_MixedViewAndFeedParameter_ViewModel.MyMethodWithCt))]
|
||||
public async Task When_MixedViewAndFeedParameter_ViewModel_CanExecuteWithParameter(string method)
|
||||
{
|
||||
await using var vm = new When_MixedViewAndFeedParameter_ViewModel.BindableWhen_MixedViewAndFeedParameter_ViewModel();
|
||||
await using var vm = new BindableWhen_MixedViewAndFeedParameter_ViewModel();
|
||||
|
||||
var commandInfo = vm.GetType().GetMember(method).Single();
|
||||
commandInfo.Should().BeAssignableTo<PropertyInfo>(because: "a command should have been generated for that method");
|
||||
|
@ -696,7 +696,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[DataRow(nameof(When_MixedViewAndFeedParameter_ViewModel.MyMethodWithCt))]
|
||||
public async Task When_MixedViewAndFeedParameter_ViewModel_CanExecuteOnlyWithParameter(string method)
|
||||
{
|
||||
await using var vm = new When_MixedViewAndFeedParameter_ViewModel.BindableWhen_MixedViewAndFeedParameter_ViewModel();
|
||||
await using var vm = new BindableWhen_MixedViewAndFeedParameter_ViewModel();
|
||||
|
||||
var commandInfo = vm.GetType().GetMember(method).Single();
|
||||
commandInfo.Should().BeAssignableTo<PropertyInfo>(because: "a command should have been generated for that method");
|
||||
|
@ -719,7 +719,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[DataRow(nameof(When_MixedViewAndFeedParameter_ViewModel.MyMethodWithCt), _viewParam, nameof(When_MixedViewAndFeedParameter_ViewModel.MyParameter), nameof(When_MixedViewAndFeedParameter_ViewModel.MyParameter2), _ct)]
|
||||
private async Task When_MixedViewAndFeedParameter_ArgsReDispatchedProperly(string method, params string[] expectedArgs)
|
||||
{
|
||||
await using var vm = new When_MixedViewAndFeedParameter_ViewModel.BindableWhen_MixedViewAndFeedParameter_ViewModel();
|
||||
await using var vm = new BindableWhen_MixedViewAndFeedParameter_ViewModel();
|
||||
|
||||
var commandInfo = vm.GetType().GetMember(method).Single();
|
||||
commandInfo.Should().BeAssignableTo<PropertyInfo>(because: "a command should have been generated for that method");
|
||||
|
@ -761,7 +761,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
GetMember(nameof(When_ImplicitCommandDisabled_ViewModel.AsyncWithParameter2)).Should().NotBeNull().And.BeAssignableTo<MethodInfo>();
|
||||
|
||||
MemberInfo GetMember(string name)
|
||||
=> typeof(When_ImplicitCommandDisabled_ViewModel.BindableWhen_ImplicitCommandDisabled_ViewModel).GetMember(name, BindingFlags.Instance | BindingFlags.Public).Single();
|
||||
=> typeof(BindableWhen_ImplicitCommandDisabled_ViewModel).GetMember(name, BindingFlags.Instance | BindingFlags.Public).Single();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -773,7 +773,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
.Subject.PropertyType.Should().BeAssignableTo(typeof(ICommand));
|
||||
|
||||
MemberInfo GetMember(string name)
|
||||
=> typeof(When_ImplicitCommandDisabled_ViewModel.BindableWhen_ImplicitCommandDisabled_ViewModel).GetMember(name, BindingFlags.Instance | BindingFlags.Public).Single();
|
||||
=> typeof(BindableWhen_ImplicitCommandDisabled_ViewModel).GetMember(name, BindingFlags.Instance | BindingFlags.Public).Single();
|
||||
}
|
||||
|
||||
[ImplicitFeedCommandParameters(false)]
|
||||
|
@ -791,7 +791,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_ImplicitFeedCommandDisabled_ViewModel_Then_ParameterNotUsed()
|
||||
{
|
||||
await using var vm = new When_ImplicitFeedCommandDisabled_ViewModel.BindableWhen_ImplicitFeedCommandDisabled_ViewModel();
|
||||
await using var vm = new BindableWhen_ImplicitFeedCommandDisabled_ViewModel();
|
||||
|
||||
var subs = GetSubCommands(vm.SyncWithParameter);
|
||||
subs.Should().HaveCount(1);
|
||||
|
@ -809,7 +809,7 @@ public partial class Given_Methods_Then_GenerateCommands : FeedUITests
|
|||
[TestMethod]
|
||||
public async Task When_ImplicitFeedCommandDisabledWithExplicitAttribute_ViewModel_Then_ParameterUsed()
|
||||
{
|
||||
await using var vm = new When_ImplicitFeedCommandDisabled_ViewModel.BindableWhen_ImplicitFeedCommandDisabled_ViewModel();
|
||||
await using var vm = new BindableWhen_ImplicitFeedCommandDisabled_ViewModel();
|
||||
|
||||
// We wait for the feed parameter to be full-filed
|
||||
await WaitFor(() => vm.WithExplicitAttribute.CanExecute(null));
|
||||
|
|
|
@ -18,7 +18,7 @@ public partial class Given_RecordWithList_Then_GenerateListOfBindable : FeedUITe
|
|||
[TestMethod]
|
||||
public async Task IsListOfBindable()
|
||||
{
|
||||
await using var sut = new MyViewModel.BindableMyViewModel();
|
||||
await using var sut = new BindableMyViewModel();
|
||||
|
||||
sut.MyFeed.Items.Should().BeAssignableTo<IEnumerable<BindableMyRecordWithListItem>>();
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public partial class Given_RecordWithList_Then_GenerateListOfBindable : FeedUITe
|
|||
[TestMethod]
|
||||
public async Task SupportsAdd()
|
||||
{
|
||||
await using var sut = new MyViewModel.BindableMyViewModel();
|
||||
await using var sut = new BindableMyViewModel();
|
||||
var args = new List<NotifyCollectionChangedEventArgs>();
|
||||
var result = sut.Model.MyFeed.Select(r => r.Items).Record();
|
||||
|
||||
|
@ -48,7 +48,7 @@ public partial class Given_RecordWithList_Then_GenerateListOfBindable : FeedUITe
|
|||
[TestMethod]
|
||||
public async Task SupportsRemove()
|
||||
{
|
||||
await using var sut = new MyViewModel.BindableMyViewModel();
|
||||
await using var sut = new BindableMyViewModel();
|
||||
var args = new List<NotifyCollectionChangedEventArgs>();
|
||||
var result = sut.Model.MyFeed.Select(r => r.Items).Record();
|
||||
|
||||
|
|
|
@ -96,21 +96,23 @@ public partial class Given_ViewModel_Then_GenerateBindable
|
|||
=> Assert.IsNotNull(GetBindable(typeof(NestedSubViewModel)));
|
||||
|
||||
private Type? GetBindable(Type vmType)
|
||||
=> vmType.GetNestedType(vmType.Name switch
|
||||
{
|
||||
{ } name when name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase) => $"Bindable{vmType.Name}",
|
||||
{ } name when name.EndsWith("Model", StringComparison.OrdinalIgnoreCase) => $"{name.Substring(0, name.Length - "Model".Length)}ViewModel",
|
||||
{ } name => $"{name}ViewModel",
|
||||
});
|
||||
=> GetBindable(vmType.FullName!);
|
||||
|
||||
private Type? GetBindable(string vmType)
|
||||
{
|
||||
var index = vmType.LastIndexOf('.');
|
||||
var bindableType = index >= 0
|
||||
? $"{vmType.Substring(0, index)}.Bindable{vmType.Substring(index + 1)}"
|
||||
: "Bindable" + vmType;
|
||||
var index = vmType.LastIndexOf('+');
|
||||
if (index >= 0)
|
||||
{
|
||||
return GetType().Assembly.GetType($"{vmType.Substring(0, index)}+Bindable{vmType.Substring(index + 1)}");
|
||||
}
|
||||
|
||||
return GetType().Assembly.GetType(bindableType);
|
||||
index = vmType.LastIndexOf('.');
|
||||
if (index >= 0)
|
||||
{
|
||||
return GetType().Assembly.GetType($"{vmType.Substring(0, index)}.Bindable{vmType.Substring(index + 1)}");
|
||||
}
|
||||
|
||||
return GetType().Assembly.GetType("Bindable" + vmType);
|
||||
}
|
||||
|
||||
public partial class Nested_ViewModel { }
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
@ -15,7 +14,7 @@ namespace Uno.Extensions.Reactive.Tests;
|
|||
|
||||
public class FeedUITests : FeedTests
|
||||
{
|
||||
private readonly Dispatcher _dispatcher = new();
|
||||
private readonly TestDispatcher _dispatcher = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
[TestInitialize]
|
||||
|
@ -115,74 +114,4 @@ public class FeedUITests : FeedTests
|
|||
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
private class Dispatcher : IDispatcherInternal, IDisposable
|
||||
{
|
||||
private readonly Thread _thread;
|
||||
private readonly Queue<Action> _queue = new();
|
||||
private readonly AutoResetEvent _evt = new(false);
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
public Dispatcher()
|
||||
{
|
||||
_thread = new Thread(Run);
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasThreadAccess => Thread.CurrentThread == _thread;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void TryEnqueue(Action action)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new InvalidOperationException("Dispatcher has already been aborted!");
|
||||
}
|
||||
|
||||
lock (_queue)
|
||||
{
|
||||
_queue.Enqueue(action);
|
||||
}
|
||||
|
||||
_evt.Set();
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
while (!_isDisposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool hasItem;
|
||||
Action? item;
|
||||
lock (_queue)
|
||||
{
|
||||
hasItem = _queue.TryDequeue(out item);
|
||||
}
|
||||
|
||||
if (hasItem)
|
||||
{
|
||||
item!();
|
||||
}
|
||||
else
|
||||
{
|
||||
_evt.WaitOne();
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
throw new InvalidOperationException("Got an exception on the UI thread", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_isDisposed = true;
|
||||
_evt.Set();
|
||||
_thread.Join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Uno.Extensions.Equality;
|
||||
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Reactive.Tests")]
|
||||
|
||||
[assembly: ImplicitKeys(IsEnabled = false)]
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Uno.Extensions.Reactive.Bindings;
|
||||
using Uno.Extensions.Reactive.Dispatching;
|
||||
|
||||
namespace Uno.Extensions.Reactive.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize this module to register services provided by this UI module for the reactive framework.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class ModuleInitializer
|
||||
{
|
||||
private static int _isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Register the <seealso cref="DispatcherQueueProvider"/> as provider of <see cref="IDispatcher"/> for the reactive platform.
|
||||
/// </summary>
|
||||
/// <remarks>This method is flagged with ModuleInitializer attribute and should not be used by application.</remarks>
|
||||
|
||||
#pragma warning disable CA2255 // The 'ModuleInitializer' attribute should not be used in libraries
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255 // The 'ModuleInitializer' attribute should not be used in libraries
|
||||
public static void Initialize()
|
||||
{
|
||||
// This method might be invoked more than once (ModuleInitializer from this assembly, the generated initializer of assemblies that depends on this assembly, ... and user application code.
|
||||
if (Interlocked.CompareExchange(ref _isInitialized, 1, 0) is 0)
|
||||
{
|
||||
DispatcherHelper.GetForCurrentThread = DispatcherQueueProvider.GetForCurrentThread;
|
||||
BindableHelper.ConfigureFactory(BindableFactory.Instance);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Uno.Extensions.Reactive.Core;
|
||||
using Uno.Extensions.Reactive.Utils;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of <see cref="IBindableFactory"/> for the UWP and WinUI platform.
|
||||
/// </summary>
|
||||
/// <remarks>This is not intended to be used by application but instead be initialized by module initializer.</remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class BindableFactory : IBindableFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The singleton instance.
|
||||
/// </summary>
|
||||
public static BindableFactory Instance { get; } = new BindableFactory();
|
||||
|
||||
private BindableFactory()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IListFeed<T> CreateList<T>(string name, IListState<T> source)
|
||||
=> new BindableListFeed<T>(name, source);
|
||||
}
|
|
@ -32,7 +32,20 @@ public sealed partial class BindableListFeed<T> : ISignal<IMessage>, IListState<
|
|||
PropertyName = propertyName;
|
||||
|
||||
_state = ctx.GetOrCreateListState(source);
|
||||
_items = CreateBindableCollection(_state, ctx);
|
||||
_items = CreateBindableCollection(_state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property backed by the object.</param>
|
||||
/// <param name="source">The source data stream.</param>
|
||||
public BindableListFeed(string propertyName, IListState<T> source)
|
||||
{
|
||||
PropertyName = propertyName;
|
||||
|
||||
_state = source;
|
||||
_items = CreateBindableCollection(source);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -81,8 +94,9 @@ public sealed partial class BindableListFeed<T> : ISignal<IMessage>, IListState<
|
|||
=> _state.UpdateMessage(updater, ct);
|
||||
|
||||
|
||||
private static BindableCollection CreateBindableCollection(IListState<T> state, SourceContext ctx)
|
||||
private static BindableCollection CreateBindableCollection(IListState<T> state)
|
||||
{
|
||||
var ctx = state.Context;
|
||||
var currentCount = 0;
|
||||
var pageTokens = new TokenSetAwaiter<PageToken>();
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ using Uno.Extensions.Reactive.Bindings.Collections.Services;
|
|||
using Uno.Extensions.Reactive.Collections;
|
||||
using Uno.Extensions.Reactive.Dispatching;
|
||||
using Uno.Extensions.Reactive.Utils;
|
||||
using ISchedulersProvider = Uno.Extensions.Reactive.Dispatching.DispatcherHelper.FindDispatcher;
|
||||
using ISchedulersProvider = Uno.Extensions.Reactive.Dispatching.FindDispatcher;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings.Collections
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Data
|
|||
{
|
||||
private readonly DataLayer? _parent;
|
||||
private readonly IServiceProvider? _services;
|
||||
private readonly IDispatcherInternal? _context;
|
||||
private readonly IDispatcher? _context;
|
||||
private readonly IBindableCollectionDataLayerStrategy _layerStrategy;
|
||||
private readonly IEnumerable<object> _facets;
|
||||
|
||||
|
@ -34,7 +34,7 @@ namespace Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Data
|
|||
|
||||
public IBindableCollectionViewSource? Parent => _parent;
|
||||
|
||||
public IDispatcherInternal? Dispatcher => _context;
|
||||
public IDispatcher? Dispatcher => _context;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a holder for the root layer of data
|
||||
|
@ -43,7 +43,7 @@ namespace Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Data
|
|||
IBindableCollectionDataLayerStrategy layerStrategy,
|
||||
IObservableCollection items,
|
||||
IServiceProvider? services,
|
||||
IDispatcherInternal? context)
|
||||
IDispatcher? context)
|
||||
{
|
||||
var holder = new DataLayer(null, services, layerStrategy, context);
|
||||
var initContext = layerStrategy.CreateUpdateContext(VisitorType.InitializeCollection, TrackingMode.Reset);
|
||||
|
@ -67,7 +67,7 @@ namespace Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Data
|
|||
return (holder, initializer);
|
||||
}
|
||||
|
||||
private DataLayer(DataLayer? parent, IServiceProvider? services, IBindableCollectionDataLayerStrategy layerStrategy, IDispatcherInternal? context)
|
||||
private DataLayer(DataLayer? parent, IServiceProvider? services, IBindableCollectionDataLayerStrategy layerStrategy, IDispatcher? context)
|
||||
{
|
||||
_context = context;
|
||||
_parent = parent;
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Facet
|
|||
private readonly EventRegistrationTokenTable<CurrentChangingEventHandler> _currentChanging = new();
|
||||
private readonly ISelectionService? _service;
|
||||
private readonly Lazy<IObservableVector<object>> _target;
|
||||
private readonly IDispatcherInternal? _dispatcher;
|
||||
private readonly IDispatcher? _dispatcher;
|
||||
|
||||
private bool _isInit;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ internal interface IBindableCollectionViewSource : IServiceProvider
|
|||
/// Gets the dispatcher to which this collection view source belongs.
|
||||
/// </summary>
|
||||
/// <remarks>This can be null if this collection belongs to background threads (uncommon).</remarks>
|
||||
IDispatcherInternal? Dispatcher { get; }
|
||||
IDispatcher? Dispatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific facet of this collection.
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Dispatching;
|
||||
|
||||
internal class DispatcherHelper
|
||||
{
|
||||
public delegate IDispatcherInternal? FindDispatcher();
|
||||
|
||||
public static IDispatcherInternal GetDispatcher()
|
||||
=> GetDispatcher(null);
|
||||
|
||||
public static IDispatcherInternal GetDispatcher(IDispatcherInternal? given)
|
||||
=> given
|
||||
?? GetForCurrentThread()
|
||||
?? throw new InvalidOperationException("Failed to get dispatcher to use. Either explicitly provide the dispatcher to use, either make sure to invoke this on the UI thread.");
|
||||
|
||||
public static FindDispatcher GetForCurrentThread = DispatcherQueueProvider.GetForCurrentThread;
|
||||
|
||||
public static bool HasThreadAccess => GetForCurrentThread()?.HasThreadAccess ?? false;
|
||||
}
|
|
@ -1,29 +1,65 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Dispatching;
|
||||
|
||||
internal static class DispatcherQueueProvider
|
||||
/// <summary>
|
||||
/// Provider of <see cref="IDispatcher"/>.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class DispatcherQueueProvider
|
||||
{
|
||||
private static readonly ThreadLocal<IDispatcherInternal?> _value = new(CreateForCurrentThread, false);
|
||||
private static readonly ThreadLocal<IDispatcher?> _value = new(CreateForCurrentThread, false);
|
||||
|
||||
public static IDispatcherInternal? GetForCurrentThread()
|
||||
/// <summary>
|
||||
/// Gets a dispatcher queue instance that will execute tasks serially on the current thread, or null if no such queue exists.
|
||||
/// </summary>
|
||||
/// <returns>The dispatcher associated to the current thread if the thread is a UI thread.</returns>
|
||||
public static IDispatcher? GetForCurrentThread()
|
||||
=> _value.Value;
|
||||
|
||||
private static IDispatcherInternal? CreateForCurrentThread()
|
||||
private static IDispatcher? CreateForCurrentThread()
|
||||
=> DispatcherQueue.GetForCurrentThread() is { } dispatcher ? new Dispatcher(dispatcher) : null;
|
||||
|
||||
private class Dispatcher : IDispatcherInternal
|
||||
private class Dispatcher : IDispatcher
|
||||
{
|
||||
private readonly DispatcherQueue _queue;
|
||||
|
||||
public Dispatcher(DispatcherQueue queue)
|
||||
=> _queue = queue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasThreadAccess => _queue.HasThreadAccess;
|
||||
|
||||
public void TryEnqueue(Action action)
|
||||
/// <inheritdoc />
|
||||
public bool TryEnqueue(Action action)
|
||||
=> _queue.TryEnqueue(() => action());
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<TResult> ExecuteAsync<TResult>(AsyncFunc<TResult> action, CancellationToken ct)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<TResult>();
|
||||
using var ctReg = ct.CanBeCanceled ? ct.Register(() => tcs.TrySetCanceled()) : default;
|
||||
|
||||
TryEnqueue(Execute);
|
||||
|
||||
return await tcs.Task;
|
||||
|
||||
async void Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.TrySetResult(await action(ct));
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
tcs.TrySetException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Dispatching;
|
||||
|
||||
// Unlike the Uno.Extensions.Core.IDispatcher, this represent only a platform agnostic access to the dispatcher.
|
||||
// It's expected to match the contract of the real dispatcher (i.e. DispatcherQueue) without any kind of decoration.
|
||||
// It's not intended to be registered in the DI/IoC and the instance is expected to be null when resolved from a background thread, like the real DispatcherQueue.
|
||||
internal interface IDispatcherInternal
|
||||
{
|
||||
public bool HasThreadAccess { get; }
|
||||
|
||||
public void TryEnqueue(Action action);
|
||||
}
|
|
@ -6,7 +6,6 @@ using Uno.Extensions.Reactive.Bindings;
|
|||
using Uno.Extensions.Reactive.Core;
|
||||
using Uno.Extensions.Reactive.Logging;
|
||||
using Uno.Extensions.Reactive.Sources;
|
||||
using Uno.Extensions.Reactive.UI.Utils;
|
||||
using Uno.Extensions.Reactive.Utils;
|
||||
#if WINUI
|
||||
using _Page = Microsoft.UI.Xaml.Controls.Page;
|
||||
|
|
|
@ -3,11 +3,6 @@
|
|||
<AssemblyName>Uno.Extensions.Reactive.UI</AssemblyName>
|
||||
<!-- Ensures the .xr.xml files are generated in a proper layout folder -->
|
||||
<GenerateLibraryLayout>true</GenerateLibraryLayout>
|
||||
|
||||
<!--
|
||||
As we are InternalsVisibleTo(Uno.Extensions.Reactive.Tests), we disable some compatibility types that are not used by Reactive package itself.
|
||||
-->
|
||||
<UnoExtensionsGeneration_DisableModuleInitializerAttribute>True</UnoExtensionsGeneration_DisableModuleInitializerAttribute>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(_IsUWP)'=='true'">
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Uno.Extensions.Equality;
|
||||
|
||||
[assembly: ImplicitKeys(IsEnabled = false)]
|
||||
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Reactive.Tests")]
|
||||
[assembly: InternalsVisibleTo("Uno.Extensions.Reactive.Testing")]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the generation tool of bindable view models
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public class BindableGenerationToolAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the version of tool that should be used to generate bindables.
|
||||
/// </summary>
|
||||
/// <remarks>Set this to 1 to use code gen used in Uno.Extensions.Reactive versions below 2.3</remarks>
|
||||
public int Version { get; init; } = 2;
|
||||
}
|
|
@ -10,11 +10,6 @@ namespace Uno.Extensions.Reactive.Config;
|
|||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public class ImplicitBindablesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the legacy pattern that was used in versions prior to 2.3.
|
||||
/// </summary>
|
||||
public const string LegacyPattern = "ViewModel$";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a bool which indicates if the generation of view models based on class names is enabled of not.
|
||||
/// </summary>
|
||||
|
@ -23,6 +18,8 @@ public class ImplicitBindablesAttribute : Attribute
|
|||
/// <summary>
|
||||
/// The patterns that the class FullName has to match to implicitly trigger view model generation.
|
||||
/// </summary>
|
||||
/// <seealso cref="BindableGenerationToolAttribute"/>
|
||||
/// <remarks>For generation tool version 2 and above.</remarks>
|
||||
public string[] Patterns { get; } = { "Model$" };
|
||||
|
||||
/// <summary>
|
|
@ -28,7 +28,10 @@ public class Bindable<T> : IBindable, INotifyPropertyChanged, IFeed<T>
|
|||
private readonly bool _hasValueProperty;
|
||||
private readonly bool _isInherited;
|
||||
|
||||
internal string PropertyName => _property.Name;
|
||||
/// <summary>
|
||||
/// Gets the name of the property backed by this bindable
|
||||
/// </summary>
|
||||
public string PropertyName => _property.Name;
|
||||
|
||||
internal bool CanWrite => _property.CanWrite;
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Uno.Extensions.Reactive.Core;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers to create bindable friendly properties.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] // Should be used by code gen only
|
||||
public class BindableHelper
|
||||
{
|
||||
private static IBindableFactory _factory = new NullBindableFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Configures the factory to use for create bindable
|
||||
/// </summary>
|
||||
/// <param name="factory"></param>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] // Should be used by module init only
|
||||
public static void ConfigureFactory(IBindableFactory factory)
|
||||
=> _factory = factory;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bindable friendly IListFeed from a given IListState
|
||||
/// </summary>
|
||||
/// <remarks>This gives the opportunity to create a platform specific bindable friendly version of collections.</remarks>
|
||||
/// <typeparam name="T">Type of the items of the collection.</typeparam>
|
||||
/// <param name="name">Name of the property backed by the resulting bindable list.</param>
|
||||
/// <param name="source">The source list state.</param>
|
||||
/// <returns>A bindable friendly list feed.</returns>
|
||||
public static IListFeed<T> CreateBindableList<T>(string name, IListState<T> source)
|
||||
=> _factory.CreateList(name, source);
|
||||
|
||||
private class NullBindableFactory : IBindableFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IListFeed<T> CreateList<T>(string name, IListState<T> source)
|
||||
=> source;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ using Uno.Extensions.Reactive.Core;
|
|||
using Uno.Extensions.Reactive.Dispatching;
|
||||
using Uno.Extensions.Reactive.Events;
|
||||
using Uno.Extensions.Reactive.Logging;
|
||||
using Uno.Extensions.Reactive.UI.Utils;
|
||||
using Uno.Extensions.Reactive.Utils;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
||||
|
||||
/// <summary>
|
||||
/// A factory of platform specific bindable friendly
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface is used to abstract the UI platform used to run the reactive framework.
|
||||
/// It is not intended to be implemented by application.
|
||||
/// </remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] // Should be used by UI module only, not apps
|
||||
public interface IBindableFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a bindable friendly IListFeed from a given IListState
|
||||
/// </summary>
|
||||
/// <remarks>This gives the opportunity to create a platform specific bindable friendly version of collections.</remarks>
|
||||
/// <typeparam name="T">Type of the items of the collection.</typeparam>
|
||||
/// <param name="name">Name of the property backed by the resulting bindable list.</param>
|
||||
/// <param name="source">The source list state.</param>
|
||||
/// <returns>A bindable friendly list feed.</returns>
|
||||
IListFeed<T> CreateList<T>(string name, IListState<T> source);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
||||
|
||||
/// <summary>
|
||||
/// Flags an object as a _model_.
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel"></typeparam>
|
||||
/// <remarks>
|
||||
/// This interface is expected to be implemented by the code gen, you should not have to implement it in your code.
|
||||
/// EVen if it's not recommended, this gives to the _Model_ to ability to interact with its _ViewModel_ if needed.
|
||||
/// </remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
public interface IModel<out TViewModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the instance of the view model associated to this model.
|
||||
/// </summary>
|
||||
TViewModel ViewModel { get; }
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
||||
|
||||
/// <summary>
|
||||
/// Flags a class as a _model_.
|
||||
/// </summary>
|
||||
/// <remarks>This attribute is added by the feeds generator on the _model_ type, you should not have to use it.</remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ModelAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Flags a class as a _model_.
|
||||
/// </summary>
|
||||
/// <param name="viewModel">The type of the _view model_.</param>
|
||||
public ModelAttribute(Type viewModel)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Uno.Extensions.Reactive.Bindings;
|
||||
|
||||
/// <summary>
|
||||
/// Flags a class as a _view model_ (a.k.a. Bindable).
|
||||
/// </summary>
|
||||
/// <remarks>This attribute is added by the feeds generator on the _view model_ type, you should not have to use it.</remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ViewModelAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Flags a class as a _view model_ (a.k.a. Bindable).
|
||||
/// </summary>
|
||||
/// <param name="model">The type of the _model_.</param>
|
||||
public ViewModelAttribute(Type model)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="Uno.Toolkit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче