committed by
GitHub
32 changed files with 1819 additions and 443 deletions
@ -0,0 +1,46 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Avalonia.Data.Converters |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A multi-value converter which calls <see cref="string.Format(string, object)"/>
|
||||
|
/// </summary>
|
||||
|
public class StringFormatMultiValueConverter : IMultiValueConverter |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="StringFormatMultiValueConverter"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="format">The format string.</param>
|
||||
|
/// <param name="inner">
|
||||
|
/// An optional inner converter to be called before the format takes place.
|
||||
|
/// </param>
|
||||
|
public StringFormatMultiValueConverter(string format, IMultiValueConverter inner) |
||||
|
{ |
||||
|
Contract.Requires<ArgumentNullException>(format != null); |
||||
|
|
||||
|
Format = format; |
||||
|
Inner = inner; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets an inner value converter which will be called before the string format takes place.
|
||||
|
/// </summary>
|
||||
|
public IMultiValueConverter Inner { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the format string.
|
||||
|
/// </summary>
|
||||
|
public string Format { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
return Inner == null |
||||
|
? string.Format(culture, Format, values.ToArray()) |
||||
|
: string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.VisualTree; |
||||
|
using Avalonia.Controls; |
||||
|
using System.Threading; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Reactive.Subjects; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Reactive; |
||||
|
using ReactiveUI; |
||||
|
using System; |
||||
|
using Avalonia.Controls.ApplicationLifetimes; |
||||
|
using Splat; |
||||
|
|
||||
|
namespace Avalonia.ReactiveUI |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for
|
||||
|
/// Avalonia applications. Call its constructor in your app's composition root,
|
||||
|
/// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method.
|
||||
|
/// </summary>
|
||||
|
public sealed class AutoSuspendHelper : IEnableLogger, IDisposable |
||||
|
{ |
||||
|
private readonly Subject<IDisposable> _shouldPersistState = new Subject<IDisposable>(); |
||||
|
private readonly Subject<Unit> _isLaunchingNew = new Subject<Unit>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AutoSuspendHelper"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="lifetime">Pass in the Application.ApplicationLifetime property.</param>
|
||||
|
public AutoSuspendHelper(IApplicationLifetime lifetime) |
||||
|
{ |
||||
|
RxApp.SuspensionHost.IsResuming = Observable.Never<Unit>(); |
||||
|
RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; |
||||
|
|
||||
|
if (lifetime is IControlledApplicationLifetime controlled) |
||||
|
{ |
||||
|
this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); |
||||
|
controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); |
||||
|
RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; |
||||
|
} |
||||
|
else if (lifetime != null) |
||||
|
{ |
||||
|
var type = lifetime.GetType().FullName; |
||||
|
var message = $"Don't know how to detect app exit event for {type}."; |
||||
|
throw new NotSupportedException(message); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var message = "ApplicationLifetime is null. " |
||||
|
+ "Ensure you are initializing AutoSuspendHelper " |
||||
|
+ "when Avalonia application initialization is completed."; |
||||
|
throw new ArgumentNullException(message); |
||||
|
} |
||||
|
|
||||
|
var errored = new Subject<Unit>(); |
||||
|
AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); |
||||
|
RxApp.SuspensionHost.ShouldInvalidateState = errored; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Call this method in your App.OnFrameworkInitializationCompleted method.
|
||||
|
/// </summary>
|
||||
|
public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disposes internally stored observers.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_shouldPersistState.Dispose(); |
||||
|
_isLaunchingNew.Dispose(); |
||||
|
} |
||||
|
|
||||
|
private void OnControlledApplicationLifetimeExit() |
||||
|
{ |
||||
|
this.Log().Debug("Received IControlledApplicationLifetime exit event."); |
||||
|
var manual = new ManualResetEvent(false); |
||||
|
_shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); |
||||
|
|
||||
|
manual.WaitOne(); |
||||
|
this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.UnitTests.Converters |
||||
|
{ |
||||
|
public class ValueConverterTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void ValueConverter_Special_Values_Work() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
||||
|
{ |
||||
|
var xaml = @"
|
||||
|
<Window xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
||||
|
xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'> |
||||
|
<TextBlock Name='textBlock' Text='{Binding Converter={x:Static c:TestConverter.Instance}, FallbackValue=bar}'/> |
||||
|
</Window>";
|
||||
|
var loader = new AvaloniaXamlLoader(); |
||||
|
var window = (Window)loader.Load(xaml); |
||||
|
var textBlock = window.FindControl<TextBlock>("textBlock"); |
||||
|
|
||||
|
window.ApplyTemplate(); |
||||
|
|
||||
|
window.DataContext = 2; |
||||
|
Assert.Equal("foo", textBlock.Text); |
||||
|
|
||||
|
window.DataContext = -3; |
||||
|
Assert.Equal("foo", textBlock.Text); |
||||
|
|
||||
|
window.DataContext = 0; |
||||
|
Assert.Equal("bar", textBlock.Text); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class TestConverter : IValueConverter |
||||
|
{ |
||||
|
public static readonly TestConverter Instance = new TestConverter(); |
||||
|
|
||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
if (value is int i) |
||||
|
{ |
||||
|
if (i > 0) |
||||
|
{ |
||||
|
return "foo"; |
||||
|
} |
||||
|
|
||||
|
if (i == 0) |
||||
|
{ |
||||
|
return AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
|
||||
|
return BindingOperations.DoNothing; |
||||
|
} |
||||
|
|
||||
|
return "(default)"; |
||||
|
} |
||||
|
|
||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Reactive.Concurrency; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.ComponentModel; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Reactive; |
||||
|
using System.Reactive.Subjects; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.Serialization; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Controls.ApplicationLifetimes; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
using Avalonia; |
||||
|
using ReactiveUI; |
||||
|
using DynamicData; |
||||
|
using Xunit; |
||||
|
using Splat; |
||||
|
|
||||
|
namespace Avalonia.ReactiveUI.UnitTests |
||||
|
{ |
||||
|
public class AutoSuspendHelperTest |
||||
|
{ |
||||
|
[DataContract] |
||||
|
public class AppState |
||||
|
{ |
||||
|
[DataMember] |
||||
|
public string Example { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class ExoticApplicationLifetimeWithoutLifecycleEvents : IDisposable, IApplicationLifetime |
||||
|
{ |
||||
|
public void Dispose() { } |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
||||
|
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
||||
|
{ |
||||
|
var isLaunchingReceived = false; |
||||
|
var application = AvaloniaLocator.Current.GetService<Application>(); |
||||
|
application.ApplicationLifetime = lifetime; |
||||
|
|
||||
|
// Initialize ReactiveUI Suspension as in real-world scenario.
|
||||
|
var suspension = new AutoSuspendHelper(application.ApplicationLifetime); |
||||
|
RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); |
||||
|
suspension.OnFrameworkInitializationCompleted(); |
||||
|
|
||||
|
Assert.True(isLaunchingReceived); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
||||
|
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
||||
|
{ |
||||
|
var shouldPersistReceived = false; |
||||
|
var application = AvaloniaLocator.Current.GetService<Application>(); |
||||
|
application.ApplicationLifetime = lifetime; |
||||
|
|
||||
|
// Initialize ReactiveUI Suspension as in real-world scenario.
|
||||
|
var suspension = new AutoSuspendHelper(application.ApplicationLifetime); |
||||
|
RxApp.SuspensionHost.CreateNewAppState = () => new AppState { Example = "Foo" }; |
||||
|
RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); |
||||
|
RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); |
||||
|
suspension.OnFrameworkInitializationCompleted(); |
||||
|
|
||||
|
lifetime.Shutdown(); |
||||
|
Assert.True(shouldPersistReceived); |
||||
|
Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState<AppState>().Example); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AutoSuspendHelper_Should_Throw_For_Not_Supported_Lifetimes() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
||||
|
using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) |
||||
|
{ |
||||
|
var application = AvaloniaLocator.Current.GetService<Application>(); |
||||
|
application.ApplicationLifetime = lifetime; |
||||
|
Assert.Throws<NotSupportedException>(() => new AutoSuspendHelper(application.ApplicationLifetime)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue