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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,888 changes: 0 additions & 1,888 deletions dotnet-install.sh

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageDescription>Provides TestScheduler extensions for testing ReactiveUI applications with Microsoft.Reactive.Testing</PackageDescription>
<PackageId>ReactiveUI.Testing.Reactive</PackageId>
<PackageTags>mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;test;testscheduler;</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Using Include="System" />
Expand Down
2 changes: 1 addition & 1 deletion src/ReactiveUI.Testing.Reactive/TestSchedulerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Reactive;
using Microsoft.Reactive.Testing;

namespace ReactiveUI.Testing.Reactive;
namespace ReactiveUI.Testing;

/// <summary>
/// Extension methods for TestScheduler from Microsoft.Reactive.Testing.
Expand Down
25 changes: 24 additions & 1 deletion src/ReactiveUI/Activation/ViewModelActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,30 @@ public IDisposable Activate()
/// </param>
public void Deactivate(bool ignoreRefCount = false)
{
if (Interlocked.Decrement(ref _refCount) == 0 || ignoreRefCount)
if (ignoreRefCount)
{
Interlocked.Exchange(ref _refCount, 0);
Interlocked.Exchange(ref _activationHandle, Disposable.Empty).Dispose();
_deactivated.OnNext(Unit.Default);
return;
}

// Guard against going negative — only decrement if current value is > 0
int current;
int next;
do
{
current = Volatile.Read(ref _refCount);
if (current <= 0)
{
return;
}

next = current - 1;
}
while (Interlocked.CompareExchange(ref _refCount, next, current) != current);

if (next == 0)
{
Interlocked.Exchange(ref _activationHandle, Disposable.Empty).Dispose();
_deactivated.OnNext(Unit.Default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ internal PropertyBinderImplementation(

// Check if we have converters or if types are assignable
var hasConverters = vmToViewConverterObj is not null && viewToVMConverterObj is not null;
var typesAreAssignable = typeof(TVProp).IsAssignableFrom(typeof(TVMProp)) && typeof(TVMProp).IsAssignableFrom(typeof(TVProp));
var typesAreAssignable = typeof(TVProp).IsAssignableFrom(typeof(TVMProp)) || typeof(TVMProp).IsAssignableFrom(typeof(TVProp));

if (!hasConverters && !typesAreAssignable)
{
Expand Down
67 changes: 67 additions & 0 deletions src/ReactiveUI/Builder/IReactiveUIBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,73 @@ IReactiveUIBuilder WithPlatformModule<T>()
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit);

/// <summary>
/// Registers a typed binding converter using the concrete type.
/// </summary>
/// <typeparam name="TFrom">The source type for the conversion.</typeparam>
/// <typeparam name="TTo">The target type for the conversion.</typeparam>
/// <param name="converter">The converter instance to register.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithConverter<TFrom, TTo>(BindingTypeConverter<TFrom, TTo> converter);

/// <summary>
/// Registers a typed binding converter using the interface.
/// </summary>
/// <param name="converter">The converter instance to register.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithConverter(IBindingTypeConverter converter);

/// <summary>
/// Registers a typed binding converter via factory (lazy instantiation).
/// </summary>
/// <typeparam name="TFrom">The source type for the conversion.</typeparam>
/// <typeparam name="TTo">The target type for the conversion.</typeparam>
/// <param name="factory">The factory function that creates the converter.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithConverter<TFrom, TTo>(Func<BindingTypeConverter<TFrom, TTo>> factory);

/// <summary>
/// Registers a typed binding converter via factory (interface, lazy instantiation).
/// </summary>
/// <param name="factory">The factory function that creates the converter.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithConverter(Func<IBindingTypeConverter> factory);

/// <summary>
/// Registers a fallback binding converter.
/// </summary>
/// <param name="converter">The fallback converter instance to register.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithFallbackConverter(IBindingFallbackConverter converter);

/// <summary>
/// Registers a fallback binding converter via factory (lazy instantiation).
/// </summary>
/// <param name="factory">The factory function that creates the fallback converter.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithFallbackConverter(Func<IBindingFallbackConverter> factory);

/// <summary>
/// Registers a set-method binding converter.
/// </summary>
/// <param name="converter">The set-method converter instance to register.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithSetMethodConverter(ISetMethodBindingConverter converter);

/// <summary>
/// Registers a set-method binding converter via factory (lazy instantiation).
/// </summary>
/// <param name="factory">The factory function that creates the set-method converter.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithSetMethodConverter(Func<ISetMethodBindingConverter> factory);

/// <summary>
/// Imports all converters from a Splat dependency resolver into the builder.
/// </summary>
/// <param name="resolver">The Splat resolver to import converters from.</param>
/// <returns>The builder instance for chaining.</returns>
IReactiveUIBuilder WithConvertersFrom(IReadonlyDependencyResolver resolver);

/// <summary>
/// Withes the views from assembly.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,23 @@ public IReactiveUIBuilder UsingSplatModule<T>(T registrationModule)
public IReactiveUIBuilder WithSuspensionHost<TAppState>() => throw new NotImplementedException();

public IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit) => throw new NotImplementedException();

public IReactiveUIBuilder WithConverter<TFrom, TTo>(BindingTypeConverter<TFrom, TTo> converter) => throw new NotImplementedException();

public IReactiveUIBuilder WithConverter(IBindingTypeConverter converter) => throw new NotImplementedException();

public IReactiveUIBuilder WithConverter<TFrom, TTo>(Func<BindingTypeConverter<TFrom, TTo>> factory) => throw new NotImplementedException();

public IReactiveUIBuilder WithConverter(Func<IBindingTypeConverter> factory) => throw new NotImplementedException();

public IReactiveUIBuilder WithFallbackConverter(IBindingFallbackConverter converter) => throw new NotImplementedException();

public IReactiveUIBuilder WithFallbackConverter(Func<IBindingFallbackConverter> factory) => throw new NotImplementedException();

public IReactiveUIBuilder WithSetMethodConverter(ISetMethodBindingConverter converter) => throw new NotImplementedException();

public IReactiveUIBuilder WithSetMethodConverter(Func<ISetMethodBindingConverter> factory) => throw new NotImplementedException();

public IReactiveUIBuilder WithConvertersFrom(IReadonlyDependencyResolver resolver) => throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,65 @@ public async Task WithConvertersFrom_ReturnsBuilderForChaining()
await Assert.That(result).IsSameReferenceAs(builder);
}

[Test]
public async Task WithFallbackConverter_Instance_ReturnsBuilderForChaining()
{
using var locator = new ModernDependencyResolver();
var builder = locator.CreateReactiveUIBuilder();
var converter = new TestFallbackConverter();

var result = builder.WithFallbackConverter(converter);

await Assert.That(result).IsSameReferenceAs(builder);
}

[Test]
public void WithFallbackConverter_Instance_WithNull_Throws()
{
using var locator = new ModernDependencyResolver();
var builder = locator.CreateReactiveUIBuilder();

Assert.Throws<ArgumentNullException>(() =>
builder.WithFallbackConverter((IBindingFallbackConverter)null!));
}

[Test]
public async Task WithFallbackConverter_Factory_ReturnsBuilderForChaining()
{
using var locator = new ModernDependencyResolver();
var builder = locator.CreateReactiveUIBuilder();

var result = builder.WithFallbackConverter(() => new TestFallbackConverter());

await Assert.That(result).IsSameReferenceAs(builder);
}

[Test]
public void WithFallbackConverter_Factory_WithNull_Throws()
{
using var locator = new ModernDependencyResolver();
var builder = locator.CreateReactiveUIBuilder();

Assert.Throws<ArgumentNullException>(() =>
builder.WithFallbackConverter((Func<IBindingFallbackConverter>)null!));
}

[Test]
public async Task WithFallbackConverter_ViaInterfaceTypedVariable_DoesNotRecurse()
{
// Regression test for https://github.com/reactiveui/ReactiveUI/issues/4293
// Calling WithFallbackConverter on an IReactiveUIBuilder-typed variable caused
// infinite recursion (StackOverflowException) because the extension method in
// BuilderMixins called itself instead of delegating to the interface method.
using var locator = new ModernDependencyResolver();
IReactiveUIBuilder builder = locator.CreateReactiveUIBuilder();
var converter = new TestFallbackConverter();

var result = builder.WithFallbackConverter(converter);

await Assert.That(result).IsSameReferenceAs(builder);
}

private sealed class TestTypedConverter : BindingTypeConverter<int, string>
{
public override int GetAffinityForObjects() => 1;
Expand All @@ -150,4 +209,15 @@ public bool TryConvertTyped(object? from, object? conversionHint, out object? re
return from != null;
}
}

private sealed class TestFallbackConverter : IBindingFallbackConverter
{
public int GetAffinityForObjects(Type fromType, Type toType) => 1;

public bool TryConvert(Type fromType, object from, Type toType, object? conversionHint, [NotNullWhen(true)] out object? result)
{
result = from;
return true;
}
}
}
2 changes: 0 additions & 2 deletions src/tests/ReactiveUI.Testing.Tests/SchedulerExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

using Microsoft.Reactive.Testing;

using ReactiveUI.Testing.Reactive;

using TUnit.Core.Executors;

namespace ReactiveUI.Testing.Tests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2040,7 +2040,14 @@ namespace ReactiveUI.Builder
ReactiveUI.Builder.IReactiveUIBuilder UsingSplatModule<T>(T registrationModule)
where T : Splat.Builder.IModule;
ReactiveUI.Builder.IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter(ReactiveUI.IBindingTypeConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter(System.Func<ReactiveUI.IBindingTypeConverter> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter<TFrom, TTo>(ReactiveUI.BindingTypeConverter<TFrom, TTo> converter);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter<TFrom, TTo>(System.Func<ReactiveUI.BindingTypeConverter<TFrom, TTo>> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithConvertersFrom(Splat.IReadonlyDependencyResolver resolver);
ReactiveUI.Builder.IReactiveUIBuilder WithExceptionHandler(System.IObserver<System.Exception> exceptionHandler);
ReactiveUI.Builder.IReactiveUIBuilder WithFallbackConverter(ReactiveUI.IBindingFallbackConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithFallbackConverter(System.Func<ReactiveUI.IBindingFallbackConverter> factory);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T>(System.Action<T?> action);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T1, T2>(System.Action<T1?, T2?> action);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T1, T2, T3>(System.Action<T1?, T2?, T3?> action);
Expand All @@ -2066,6 +2073,8 @@ namespace ReactiveUI.Builder
ReactiveUI.Builder.IReactiveUIBuilder WithPlatformServices();
ReactiveUI.Builder.IReactiveUIBuilder WithRegistration(System.Action<Splat.IMutableDependencyResolver> configureAction);
ReactiveUI.Builder.IReactiveUIBuilder WithRegistrationOnBuild(System.Action<Splat.IMutableDependencyResolver> configureAction);
ReactiveUI.Builder.IReactiveUIBuilder WithSetMethodConverter(ReactiveUI.ISetMethodBindingConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithSetMethodConverter(System.Func<ReactiveUI.ISetMethodBindingConverter> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithSuspensionHost();
ReactiveUI.Builder.IReactiveUIBuilder WithSuspensionHost<TAppState>();
ReactiveUI.Builder.IReactiveUIBuilder WithTaskPoolScheduler(System.Reactive.Concurrency.IScheduler scheduler, bool setRxApp = true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2040,7 +2040,14 @@ namespace ReactiveUI.Builder
ReactiveUI.Builder.IReactiveUIBuilder UsingSplatModule<T>(T registrationModule)
where T : Splat.Builder.IModule;
ReactiveUI.Builder.IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter(ReactiveUI.IBindingTypeConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter(System.Func<ReactiveUI.IBindingTypeConverter> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter<TFrom, TTo>(ReactiveUI.BindingTypeConverter<TFrom, TTo> converter);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter<TFrom, TTo>(System.Func<ReactiveUI.BindingTypeConverter<TFrom, TTo>> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithConvertersFrom(Splat.IReadonlyDependencyResolver resolver);
ReactiveUI.Builder.IReactiveUIBuilder WithExceptionHandler(System.IObserver<System.Exception> exceptionHandler);
ReactiveUI.Builder.IReactiveUIBuilder WithFallbackConverter(ReactiveUI.IBindingFallbackConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithFallbackConverter(System.Func<ReactiveUI.IBindingFallbackConverter> factory);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T>(System.Action<T?> action);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T1, T2>(System.Action<T1?, T2?> action);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T1, T2, T3>(System.Action<T1?, T2?, T3?> action);
Expand All @@ -2066,6 +2073,8 @@ namespace ReactiveUI.Builder
ReactiveUI.Builder.IReactiveUIBuilder WithPlatformServices();
ReactiveUI.Builder.IReactiveUIBuilder WithRegistration(System.Action<Splat.IMutableDependencyResolver> configureAction);
ReactiveUI.Builder.IReactiveUIBuilder WithRegistrationOnBuild(System.Action<Splat.IMutableDependencyResolver> configureAction);
ReactiveUI.Builder.IReactiveUIBuilder WithSetMethodConverter(ReactiveUI.ISetMethodBindingConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithSetMethodConverter(System.Func<ReactiveUI.ISetMethodBindingConverter> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithSuspensionHost();
ReactiveUI.Builder.IReactiveUIBuilder WithSuspensionHost<TAppState>();
ReactiveUI.Builder.IReactiveUIBuilder WithTaskPoolScheduler(System.Reactive.Concurrency.IScheduler scheduler, bool setRxApp = true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2040,7 +2040,14 @@ namespace ReactiveUI.Builder
ReactiveUI.Builder.IReactiveUIBuilder UsingSplatModule<T>(T registrationModule)
where T : Splat.Builder.IModule;
ReactiveUI.Builder.IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter(ReactiveUI.IBindingTypeConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter(System.Func<ReactiveUI.IBindingTypeConverter> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter<TFrom, TTo>(ReactiveUI.BindingTypeConverter<TFrom, TTo> converter);
ReactiveUI.Builder.IReactiveUIBuilder WithConverter<TFrom, TTo>(System.Func<ReactiveUI.BindingTypeConverter<TFrom, TTo>> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithConvertersFrom(Splat.IReadonlyDependencyResolver resolver);
ReactiveUI.Builder.IReactiveUIBuilder WithExceptionHandler(System.IObserver<System.Exception> exceptionHandler);
ReactiveUI.Builder.IReactiveUIBuilder WithFallbackConverter(ReactiveUI.IBindingFallbackConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithFallbackConverter(System.Func<ReactiveUI.IBindingFallbackConverter> factory);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T>(System.Action<T?> action);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T1, T2>(System.Action<T1?, T2?> action);
ReactiveUI.Builder.IReactiveUIInstance WithInstance<T1, T2, T3>(System.Action<T1?, T2?, T3?> action);
Expand All @@ -2066,6 +2073,8 @@ namespace ReactiveUI.Builder
ReactiveUI.Builder.IReactiveUIBuilder WithPlatformServices();
ReactiveUI.Builder.IReactiveUIBuilder WithRegistration(System.Action<Splat.IMutableDependencyResolver> configureAction);
ReactiveUI.Builder.IReactiveUIBuilder WithRegistrationOnBuild(System.Action<Splat.IMutableDependencyResolver> configureAction);
ReactiveUI.Builder.IReactiveUIBuilder WithSetMethodConverter(ReactiveUI.ISetMethodBindingConverter converter);
ReactiveUI.Builder.IReactiveUIBuilder WithSetMethodConverter(System.Func<ReactiveUI.ISetMethodBindingConverter> factory);
ReactiveUI.Builder.IReactiveUIBuilder WithSuspensionHost();
ReactiveUI.Builder.IReactiveUIBuilder WithSuspensionHost<TAppState>();
ReactiveUI.Builder.IReactiveUIBuilder WithTaskPoolScheduler(System.Reactive.Concurrency.IScheduler scheduler, bool setRxApp = true);
Expand Down
12 changes: 9 additions & 3 deletions src/tests/ReactiveUI.Wpf.Tests/Wpf/WpfActiveContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,15 @@ public async Task ResolveNoneWithFallbackBypass()
public async Task DummySuspensionDriverTest()
{
var dsd = new DummySuspensionDriver();
dsd.LoadState().Select(static _ => 1).Subscribe(async static v => await Assert.That(v).IsEqualTo(1));
dsd.SaveState("Save Me").Select(static _ => 2).Subscribe(async static v => await Assert.That(v).IsEqualTo(2));
dsd.InvalidateState().Select(static _ => 3).Subscribe(async static v => await Assert.That(v).IsEqualTo(3));
int? loadResult = null;
int? saveResult = null;
int? invalidateResult = null;
dsd.LoadState().Select(static _ => 1).Subscribe(v => loadResult = v);
dsd.SaveState("Save Me").Select(static _ => 2).Subscribe(v => saveResult = v);
dsd.InvalidateState().Select(static _ => 3).Subscribe(v => invalidateResult = v);
await Assert.That(loadResult).IsEqualTo(1);
await Assert.That(saveResult).IsEqualTo(2);
await Assert.That(invalidateResult).IsEqualTo(3);
}

public class ViewBRegisteredExecutor : STAThreadExecutor
Expand Down
Loading