Merge pull request #917 from unoplatform/dev/dr/corePresentation

feat(reactive): Allow generation of VM on non UI assembly
This commit is contained in:
David 2022-11-17 16:58:56 -05:00 коммит произвёл GitHub
Родитель 2d0a5e9d36 a7fcce6fe6
Коммит 9e77ddf64d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
116 изменённых файлов: 1290 добавлений и 762 удалений

Просмотреть файл

@ -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)' &lt; '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>

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше