committed by
GitHub
55 changed files with 111 additions and 2615 deletions
@ -1,5 +0,0 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="ReactiveUI" Version="20.1.1" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -1,45 +1,58 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Generators.Sandbox.Controls" |
|||
x:Class="Generators.Sandbox.Controls.SignUpView"> |
|||
<StackPanel> |
|||
xmlns:vm="clr-namespace:Generators.Sandbox.ViewModels" |
|||
x:Class="Generators.Sandbox.Controls.SignUpView" |
|||
x:DataType="vm:SignUpViewModel"> |
|||
<UserControl.Styles> |
|||
<Style Selector="DataValidationErrors"> |
|||
<Setter Property="ErrorTemplate"> |
|||
<Setter.Value> |
|||
<DataTemplate /> |
|||
</Setter.Value> |
|||
</Setter> |
|||
</Style> |
|||
</UserControl.Styles> |
|||
<StackPanel> |
|||
<controls:CustomTextBox Margin="0 10 0 0" |
|||
x:Name="UserNameTextBox" |
|||
Text="{Binding UserName}" |
|||
Watermark="Please, enter user name..." |
|||
UseFloatingWatermark="True" /> |
|||
<TextBlock x:Name="UserNameValidation" |
|||
<TextBlock x:Name="UserNameValidation" |
|||
Text="{Binding UserNameValidation}" |
|||
Foreground="Green" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="PasswordTextBox" |
|||
Text="{Binding Password}" |
|||
Watermark="Please, enter your password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="PasswordValidation" |
|||
<TextBlock x:Name="PasswordValidation" |
|||
Text="{Binding PasswordValidation}" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="ConfirmPasswordTextBox" |
|||
Text="{Binding ConfirmPassword}" |
|||
Watermark="Please, confirm the password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="ConfirmPasswordValidation" |
|||
Text="{Binding ConfirmPasswordValidation}" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBlock> |
|||
<TextBlock.Inlines> |
|||
<InlineCollection> |
|||
<Run x:Name="SignUpButtonDescription" /> |
|||
</InlineCollection> |
|||
</TextBlock.Inlines> |
|||
</TextBlock> |
|||
<TextBlock Text="Press the button below to sign up." /> |
|||
<Button Margin="0 10 0 5" |
|||
Content="Sign up" |
|||
Command="{Binding SignUp}" |
|||
x:Name="SignUpButton" /> |
|||
<TextBlock x:Name="CompoundValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
FontSize="12" |
|||
Text="{Binding CompoundValidation}" /> |
|||
</StackPanel> |
|||
</UserControl> |
|||
|
|||
@ -1,70 +1,76 @@ |
|||
using System.Reactive; |
|||
using ReactiveUI; |
|||
using ReactiveUI.Validation.Extensions; |
|||
using ReactiveUI.Validation.Helpers; |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using CommunityToolkit.Mvvm.ComponentModel; |
|||
using CommunityToolkit.Mvvm.Input; |
|||
|
|||
namespace Generators.Sandbox.ViewModels; |
|||
|
|||
public class SignUpViewModel : ReactiveValidationObject |
|||
public class SignUpViewModel : ObservableValidator |
|||
{ |
|||
private string _userName = string.Empty; |
|||
private string _password = string.Empty; |
|||
private string _confirmPassword = string.Empty; |
|||
|
|||
public SignUpViewModel() |
|||
{ |
|||
this.ValidationRule( |
|||
vm => vm.UserName, |
|||
name => !string.IsNullOrWhiteSpace(name), |
|||
"UserName is required."); |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.Password, |
|||
password => !string.IsNullOrWhiteSpace(password), |
|||
"Password is required."); |
|||
UserName = "Joseph!"; |
|||
Password = "1234"; |
|||
ConfirmPassword = "1234"; |
|||
SignUp = new RelayCommand(() => { }, () => !HasErrors); |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.Password, |
|||
password => password?.Length > 2, |
|||
password => $"Password should be longer, current length: {password.Length}"); |
|||
ErrorsChanged += OnErrorsChanged; |
|||
} |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.ConfirmPassword, |
|||
confirmation => !string.IsNullOrWhiteSpace(confirmation), |
|||
"Confirm password field is required."); |
|||
public RelayCommand SignUp { get; } |
|||
|
|||
var passwordsObservable = |
|||
this.WhenAnyValue( |
|||
x => x.Password, |
|||
x => x.ConfirmPassword, |
|||
(password, confirmation) => |
|||
password == confirmation); |
|||
[Required] |
|||
public string? UserName { |
|||
get; |
|||
set => SetProperty(ref field, value, validate: true); |
|||
} |
|||
|
|||
this.ValidationRule( |
|||
vm => vm.ConfirmPassword, |
|||
passwordsObservable, |
|||
"Passwords must match."); |
|||
public string? UserNameValidation |
|||
=> GetValidationMessage(nameof(UserName)); |
|||
|
|||
SignUp = ReactiveCommand.Create(() => {}, this.IsValid()); |
|||
[Required] |
|||
[MinLength(2)] |
|||
public string? Password |
|||
{ |
|||
get; |
|||
set |
|||
{ |
|||
if (SetProperty(ref field, value, validate: true)) |
|||
ValidateProperty(ConfirmPassword, nameof(ConfirmPassword)); |
|||
} |
|||
} |
|||
|
|||
public ReactiveCommand<Unit, Unit> SignUp { get; } |
|||
public string? PasswordValidation |
|||
=> GetValidationMessage(nameof(Password)); |
|||
|
|||
public string UserName |
|||
[Required] |
|||
[Compare(nameof(Password))] |
|||
public string? ConfirmPassword |
|||
{ |
|||
get => _userName; |
|||
set => this.RaiseAndSetIfChanged(ref _userName, value); |
|||
get; |
|||
set => SetProperty(ref field, value, validate: true); |
|||
} |
|||
|
|||
public string Password |
|||
public string? ConfirmPasswordValidation |
|||
=> GetValidationMessage(nameof(ConfirmPassword)); |
|||
|
|||
public string? CompoundValidation |
|||
=> GetValidationMessage(null); |
|||
|
|||
private void OnErrorsChanged(object? sender, DataErrorsChangedEventArgs e) |
|||
{ |
|||
get => _password; |
|||
set => this.RaiseAndSetIfChanged(ref _password, value); |
|||
if (e.PropertyName is not null) |
|||
OnPropertyChanged(e.PropertyName + "Validation"); |
|||
|
|||
OnPropertyChanged(CompoundValidation); |
|||
SignUp.NotifyCanExecuteChanged(); |
|||
} |
|||
|
|||
public string ConfirmPassword |
|||
private string? GetValidationMessage(string? propertyName) |
|||
{ |
|||
get => _confirmPassword; |
|||
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value); |
|||
var message = string.Join(Environment.NewLine, GetErrors(propertyName).Select(v => v.ErrorMessage)); |
|||
return string.IsNullOrEmpty(message) ? null : message; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,8 +0,0 @@ |
|||
<Application |
|||
xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ReactiveUIDemo.App"> |
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -1,37 +0,0 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.ReactiveUI; |
|||
using ReactiveUI; |
|||
using ReactiveUIDemo.ViewModels; |
|||
using ReactiveUIDemo.Views; |
|||
using Splat; |
|||
|
|||
namespace ReactiveUIDemo |
|||
{ |
|||
public class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor<FooViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor<BarViewModel>)); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |
|||
desktop.MainWindow = new MainWindow(); |
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
|
|||
public static int Main(string[] args) |
|||
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
|
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.UseReactiveUI() |
|||
.LogToTrace(); |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' |
|||
x:Class="ReactiveUIDemo.MainWindow" |
|||
xmlns:vm="using:ReactiveUIDemo.ViewModels" |
|||
xmlns:rxui="using:Avalonia.ReactiveUI" |
|||
Title="AvaloniaUI ReactiveUI Demo" |
|||
x:DataType="vm:MainWindowViewModel"> |
|||
<TabControl TabStripPlacement="Left"> |
|||
<TabItem Header="RoutedViewHost"> |
|||
<DockPanel DataContext="{Binding RoutedViewHost}"> |
|||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8"> |
|||
<Button Command="{Binding ShowFoo}">Foo</Button> |
|||
<Button Command="{Binding ShowBar}">Bar</Button> |
|||
</StackPanel> |
|||
<rxui:RoutedViewHost Router="{Binding Router}"/> |
|||
</DockPanel> |
|||
</TabItem> |
|||
</TabControl> |
|||
</Window> |
|||
@ -1,22 +0,0 @@ |
|||
using ReactiveUIDemo.ViewModels; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ReactiveUIDemo |
|||
{ |
|||
public class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
this.InitializeComponent(); |
|||
this.DataContext = new MainWindowViewModel(); |
|||
this.AttachDevTools(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Update="Views\BarView.axaml.cs"> |
|||
<DependentUpon>BarView.axaml</DependentUpon> |
|||
</Compile> |
|||
<Compile Update="Views\FooView.axaml.cs"> |
|||
<DependentUpon>FooView.axaml</DependentUpon> |
|||
</Compile> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\build\ReactiveUI.props" /> |
|||
</Project> |
|||
@ -1,11 +0,0 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class BarViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public BarViewModel(IScreen screen) => HostScreen = screen; |
|||
public string UrlPathSegment => "Bar"; |
|||
public IScreen HostScreen { get; } |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class FooViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public FooViewModel(IScreen screen) => HostScreen = screen; |
|||
public string UrlPathSegment => "Foo"; |
|||
public IScreen HostScreen { get; } |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class MainWindowViewModel : ReactiveObject |
|||
{ |
|||
public RoutedViewHostPageViewModel RoutedViewHost { get; } = new(); |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen |
|||
{ |
|||
public RoutedViewHostPageViewModel() |
|||
{ |
|||
Foo = new(this); |
|||
Bar = new(this); |
|||
Router.Navigate.Execute(Foo); |
|||
} |
|||
|
|||
public RoutingState Router { get; } = new(); |
|||
public FooViewModel Foo { get; } |
|||
public BarViewModel Bar { get; } |
|||
|
|||
public void ShowFoo() => Router.Navigate.Execute(Foo); |
|||
public void ShowBar() => Router.Navigate.Execute(Bar); |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ReactiveUIDemo.Views.BarView" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<Border Background="Blue"> |
|||
<TextBlock HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
FontSize="48"> |
|||
Bar! |
|||
</TextBlock> |
|||
</Border> |
|||
</UserControl> |
|||
@ -1,28 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ReactiveUI; |
|||
using ReactiveUIDemo.ViewModels; |
|||
|
|||
namespace ReactiveUIDemo.Views |
|||
{ |
|||
internal partial class BarView : UserControl, IViewFor<BarViewModel> |
|||
{ |
|||
public BarView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
public BarViewModel? ViewModel { get; set; } |
|||
|
|||
object? IViewFor.ViewModel |
|||
{ |
|||
get => ViewModel; |
|||
set => ViewModel = (BarViewModel?)value; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ReactiveUIDemo.Views.FooView" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<Border Background="Red"> |
|||
<TextBlock HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
FontSize="48"> |
|||
Foo! |
|||
</TextBlock> |
|||
</Border> |
|||
</UserControl> |
|||
@ -1,28 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ReactiveUI; |
|||
using ReactiveUIDemo.ViewModels; |
|||
|
|||
namespace ReactiveUIDemo.Views |
|||
{ |
|||
internal partial class FooView : UserControl, IViewFor<FooViewModel> |
|||
{ |
|||
public FooView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
public FooViewModel? ViewModel { get; set; } |
|||
|
|||
object? IViewFor.ViewModel |
|||
{ |
|||
get => ViewModel; |
|||
set => ViewModel = (FooViewModel?)value; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Threading; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
public static class AppBuilderExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia
|
|||
/// scheduler, an activation for view fetcher, a template binding hook. Remember
|
|||
/// to call this method if you are using ReactiveUI in your application.
|
|||
/// </summary>
|
|||
public static AppBuilder UseReactiveUI(this AppBuilder builder) => |
|||
builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => |
|||
{ |
|||
if (Locator.CurrentMutable is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); |
|||
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); |
|||
})); |
|||
} |
|||
} |
|||
@ -1,4 +0,0 @@ |
|||
using System.Reflection; |
|||
using Avalonia.Metadata; |
|||
|
|||
[assembly: XmlnsDefinition("http://reactiveui.net", "Avalonia.ReactiveUI")] |
|||
@ -1,59 +0,0 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Markup.Xaml.Templates; |
|||
using ReactiveUI; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls
|
|||
/// that don't have DataTemplates, and assigns a default DataTemplate that
|
|||
/// loads the View associated with each ViewModel.
|
|||
/// </summary>
|
|||
public class AutoDataTemplateBindingHook : IPropertyBindingHook |
|||
{ |
|||
private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate<object>((x, _) => |
|||
{ |
|||
var control = new ViewModelViewHost(); |
|||
var context = control.GetObservable(Control.DataContextProperty); |
|||
control.Bind(ViewModelViewHost.ViewModelProperty, context); |
|||
control.HorizontalContentAlignment = HorizontalAlignment.Stretch; |
|||
control.VerticalContentAlignment = VerticalAlignment.Stretch; |
|||
return control; |
|||
}, |
|||
true); |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool ExecuteHook( |
|||
object? source, object target, |
|||
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties, |
|||
Func<IObservedChange<object, object>[]> getCurrentViewProperties, |
|||
BindingDirection direction) |
|||
{ |
|||
var viewProperties = getCurrentViewProperties(); |
|||
var lastViewProperty = viewProperties.LastOrDefault(); |
|||
var itemsControl = lastViewProperty?.Sender as ItemsControl; |
|||
if (itemsControl == null) |
|||
return true; |
|||
|
|||
var propertyName = viewProperties.Last().GetPropertyName(); |
|||
if (propertyName != "Items" && |
|||
propertyName != "ItemsSource") |
|||
return true; |
|||
|
|||
if (itemsControl.ItemTemplate != null) |
|||
return true; |
|||
|
|||
if (itemsControl.DataTemplates != null && |
|||
itemsControl.DataTemplates.Count > 0) |
|||
return true; |
|||
|
|||
itemsControl.ItemTemplate = DefaultItemTemplate; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
using Avalonia; |
|||
using Avalonia.VisualTree; |
|||
using Avalonia.Controls; |
|||
using System.Threading; |
|||
using System.Reactive; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Subjects; |
|||
using System.Reactive.Linq; |
|||
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 (Avalonia.Controls.Design.IsDesignMode) |
|||
{ |
|||
this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state."); |
|||
RxApp.SuspensionHost.ShouldPersistState = Observable.Never<IDisposable>(); |
|||
} |
|||
else 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 " |
|||
+ "after 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."); |
|||
} |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0</TargetFrameworks> |
|||
<SignAssembly>false</SignAssembly> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Project="..\..\build\ReactiveUI.props" /> |
|||
<Import Project="..\..\build\NullableEnable.props" /> |
|||
<Import Project="..\..\build\TrimmingEnable.props" /> |
|||
<Import Project="..\..\build\DevAnalyzers.props" /> |
|||
</Project> |
|||
@ -1,75 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.VisualTree; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Interactivity; |
|||
using ReactiveUI; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// Determines when Avalonia IVisuals get activated.
|
|||
/// </summary>
|
|||
public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher |
|||
{ |
|||
/// <summary>
|
|||
/// Returns affinity for view.
|
|||
/// </summary>
|
|||
public int GetAffinityForView(Type view) |
|||
{ |
|||
return typeof(Visual).IsAssignableFrom(view) ? 10 : 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns activation observable for activatable Avalonia view.
|
|||
/// </summary>
|
|||
public IObservable<bool> GetActivationForView(IActivatableView view) |
|||
{ |
|||
if (!(view is Visual visual)) return Observable.Return(false); |
|||
if (view is Control control) return GetActivationForControl(control); |
|||
return GetActivationForVisual(visual); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Listens to Loaded and Unloaded
|
|||
/// events for Avalonia Control.
|
|||
/// </summary>
|
|||
private IObservable<bool> GetActivationForControl(Control control) |
|||
{ |
|||
var controlLoaded = Observable |
|||
.FromEventPattern<RoutedEventArgs>( |
|||
x => control.Loaded += x, |
|||
x => control.Loaded -= x) |
|||
.Select(args => true); |
|||
var controlUnloaded = Observable |
|||
.FromEventPattern<RoutedEventArgs>( |
|||
x => control.Unloaded += x, |
|||
x => control.Unloaded -= x) |
|||
.Select(args => false); |
|||
return controlLoaded |
|||
.Merge(controlUnloaded) |
|||
.DistinctUntilChanged(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Listens to AttachedToVisualTree and DetachedFromVisualTree
|
|||
/// events for Avalonia IVisuals.
|
|||
/// </summary>
|
|||
private IObservable<bool> GetActivationForVisual(Visual visual) |
|||
{ |
|||
var visualLoaded = Observable |
|||
.FromEventPattern<VisualTreeAttachmentEventArgs>( |
|||
x => visual.AttachedToVisualTree += x, |
|||
x => visual.AttachedToVisualTree -= x) |
|||
.Select(args => true); |
|||
var visualUnloaded = Observable |
|||
.FromEventPattern<VisualTreeAttachmentEventArgs>( |
|||
x => visual.DetachedFromVisualTree += x, |
|||
x => visual.DetachedFromVisualTree -= x) |
|||
.Select(args => false); |
|||
return visualLoaded |
|||
.Merge(visualUnloaded) |
|||
.DistinctUntilChanged(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,110 +0,0 @@ |
|||
using System.Reactive; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.ReactiveUI; |
|||
|
|||
public static class AvaloniaObjectReactiveExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a subject for an <see cref="AvaloniaProperty"/>.
|
|||
/// </summary>
|
|||
/// <param name="o">The object.</param>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="priority">
|
|||
/// The priority with which binding values are written to the object.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the
|
|||
/// property.
|
|||
/// </returns>
|
|||
public static ISubject<object?> GetSubject( |
|||
this AvaloniaObject o, |
|||
AvaloniaProperty property, |
|||
BindingPriority priority = BindingPriority.LocalValue) |
|||
{ |
|||
return Subject.Create<object?>( |
|||
Observer.Create<object?>(x => o.SetValue(property, x, priority)), |
|||
o.GetObservable(property)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a subject for an <see cref="AvaloniaProperty"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
/// <param name="o">The object.</param>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="priority">
|
|||
/// The priority with which binding values are written to the object.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the
|
|||
/// property.
|
|||
/// </returns>
|
|||
public static ISubject<T> GetSubject<T>( |
|||
this AvaloniaObject o, |
|||
AvaloniaProperty<T> property, |
|||
BindingPriority priority = BindingPriority.LocalValue) |
|||
{ |
|||
return Subject.Create<T>( |
|||
Observer.Create<T>(x => o.SetValue(property, x, priority)), |
|||
o.GetObservable(property)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
|
|||
/// </summary>
|
|||
/// <param name="o">The object.</param>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="priority">
|
|||
/// The priority with which binding values are written to the object.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the
|
|||
/// property.
|
|||
/// </returns>
|
|||
public static ISubject<BindingValue<object?>> GetBindingSubject( |
|||
this AvaloniaObject o, |
|||
AvaloniaProperty property, |
|||
BindingPriority priority = BindingPriority.LocalValue) |
|||
{ |
|||
return Subject.Create<BindingValue<object?>>( |
|||
Observer.Create<BindingValue<object?>>(x => |
|||
{ |
|||
if (x.HasValue) |
|||
{ |
|||
o.SetValue(property, x.Value, priority); |
|||
} |
|||
}), |
|||
o.GetBindingObservable(property)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property type.</typeparam>
|
|||
/// <param name="o">The object.</param>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="priority">
|
|||
/// The priority with which binding values are written to the object.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the
|
|||
/// property.
|
|||
/// </returns>
|
|||
public static ISubject<BindingValue<T>> GetBindingSubject<T>( |
|||
this AvaloniaObject o, |
|||
AvaloniaProperty<T> property, |
|||
BindingPriority priority = BindingPriority.LocalValue) |
|||
{ |
|||
return Subject.Create<BindingValue<T>>( |
|||
Observer.Create<BindingValue<T>>(x => |
|||
{ |
|||
if (x.HasValue) |
|||
{ |
|||
o.SetValue(property, x.Value, priority); |
|||
} |
|||
}), |
|||
o.GetBindingObservable(property)); |
|||
} |
|||
} |
|||
@ -1,92 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Concurrency; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// A reactive scheduler that uses Avalonia's <see cref="Dispatcher"/>.
|
|||
/// </summary>
|
|||
public class AvaloniaScheduler : LocalScheduler |
|||
{ |
|||
/// <summary>
|
|||
/// Users can schedule actions on the dispatcher thread while being on the correct thread already.
|
|||
/// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases.
|
|||
/// To prevent this we are limiting amount of reentrant calls to <see cref="Schedule{TState}"/> before we will
|
|||
/// schedule on a dispatcher anyway.
|
|||
/// </summary>
|
|||
private const int MaxReentrantSchedules = 32; |
|||
|
|||
private int _reentrancyGuard; |
|||
|
|||
/// <summary>
|
|||
/// The instance of the <see cref="AvaloniaScheduler"/>.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaScheduler Instance = new AvaloniaScheduler(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaScheduler"/> class.
|
|||
/// </summary>
|
|||
private AvaloniaScheduler() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action) |
|||
{ |
|||
IDisposable PostOnDispatcher() |
|||
{ |
|||
var composite = new CompositeDisposable(2); |
|||
|
|||
var cancellation = new CancellationDisposable(); |
|||
|
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
if (!cancellation.Token.IsCancellationRequested) |
|||
{ |
|||
composite.Add(action(this, state)); |
|||
} |
|||
}, DispatcherPriority.Background); |
|||
|
|||
composite.Add(cancellation); |
|||
|
|||
return composite; |
|||
} |
|||
|
|||
if (dueTime == TimeSpan.Zero) |
|||
{ |
|||
if (!Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
return PostOnDispatcher(); |
|||
} |
|||
else |
|||
{ |
|||
if (_reentrancyGuard >= MaxReentrantSchedules) |
|||
{ |
|||
return PostOnDispatcher(); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
_reentrancyGuard++; |
|||
|
|||
return action(this, state); |
|||
} |
|||
finally |
|||
{ |
|||
_reentrancyGuard--; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var composite = new CompositeDisposable(2); |
|||
|
|||
composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); |
|||
|
|||
return composite; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,66 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using ReactiveUI; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// A ReactiveUI <see cref="UserControl"/> that implements the <see cref="IViewFor{TViewModel}"/> interface and
|
|||
/// will activate your ViewModel automatically if the view model implements <see cref="IActivatableViewModel"/>.
|
|||
/// When the DataContext property changes, this class will update the ViewModel property with the new DataContext
|
|||
/// value, and vice versa.
|
|||
/// </summary>
|
|||
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
|
|||
public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class |
|||
{ |
|||
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] |
|||
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty |
|||
.Register<ReactiveUserControl<TViewModel>, TViewModel?>(nameof(ViewModel)); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ReactiveUserControl{TViewModel}"/> class.
|
|||
/// </summary>
|
|||
public ReactiveUserControl() |
|||
{ |
|||
// This WhenActivated block calls ViewModel's WhenActivated
|
|||
// block if the ViewModel implements IActivatableViewModel.
|
|||
this.WhenActivated(disposables => { }); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The ViewModel.
|
|||
/// </summary>
|
|||
public TViewModel? ViewModel |
|||
{ |
|||
get => GetValue(ViewModelProperty); |
|||
set => SetValue(ViewModelProperty, value); |
|||
} |
|||
|
|||
object? IViewFor.ViewModel |
|||
{ |
|||
get => ViewModel; |
|||
set => ViewModel = (TViewModel?)value; |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == DataContextProperty) |
|||
{ |
|||
if (ReferenceEquals(change.OldValue, ViewModel) |
|||
&& change.NewValue is null or TViewModel) |
|||
{ |
|||
SetCurrentValue(ViewModelProperty, change.NewValue); |
|||
} |
|||
} |
|||
else if (change.Property == ViewModelProperty) |
|||
{ |
|||
if (ReferenceEquals(change.OldValue, DataContext)) |
|||
{ |
|||
SetCurrentValue(DataContextProperty, change.NewValue); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,66 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using ReactiveUI; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// A ReactiveUI <see cref="Window"/> that implements the <see cref="IViewFor{TViewModel}"/> interface and will
|
|||
/// activate your ViewModel automatically if the view model implements <see cref="IActivatableViewModel"/>. When
|
|||
/// the DataContext property changes, this class will update the ViewModel property with the new DataContext value,
|
|||
/// and vice versa.
|
|||
/// </summary>
|
|||
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
|
|||
public class ReactiveWindow<TViewModel> : Window, IViewFor<TViewModel> where TViewModel : class |
|||
{ |
|||
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] |
|||
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty |
|||
.Register<ReactiveWindow<TViewModel>, TViewModel?>(nameof(ViewModel)); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ReactiveWindow{TViewModel}"/> class.
|
|||
/// </summary>
|
|||
public ReactiveWindow() |
|||
{ |
|||
// This WhenActivated block calls ViewModel's WhenActivated
|
|||
// block if the ViewModel implements IActivatableViewModel.
|
|||
this.WhenActivated(disposables => { }); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The ViewModel.
|
|||
/// </summary>
|
|||
public TViewModel? ViewModel |
|||
{ |
|||
get => GetValue(ViewModelProperty); |
|||
set => SetValue(ViewModelProperty, value); |
|||
} |
|||
|
|||
object? IViewFor.ViewModel |
|||
{ |
|||
get => ViewModel; |
|||
set => ViewModel = (TViewModel?)value; |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == DataContextProperty) |
|||
{ |
|||
if (ReferenceEquals(change.OldValue, ViewModel) |
|||
&& change.NewValue is null or TViewModel) |
|||
{ |
|||
SetCurrentValue(ViewModelProperty, change.NewValue); |
|||
} |
|||
} |
|||
else if (change.Property == ViewModelProperty) |
|||
{ |
|||
if (ReferenceEquals(change.OldValue, DataContext)) |
|||
{ |
|||
SetCurrentValue(DataContextProperty, change.NewValue); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,184 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
using Avalonia; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// This control hosts the View associated with ReactiveUI RoutingState,
|
|||
/// and will display the View and wire up the ViewModel whenever a new
|
|||
/// ViewModel is navigated to. Nested routing is also supported.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// ReactiveUI routing consists of an IScreen that contains current
|
|||
/// RoutingState, several IRoutableViewModels, and a platform-specific
|
|||
/// XAML control called RoutedViewHost.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// RoutingState manages the ViewModel navigation stack and allows
|
|||
/// ViewModels to navigate to other ViewModels. IScreen is the root of
|
|||
/// a navigation stack; despite the name, its views don't have to occupy
|
|||
/// the whole screen. RoutedViewHost monitors an instance of RoutingState,
|
|||
/// responding to any changes in the navigation stack by creating and
|
|||
/// embedding the appropriate view.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Place this control to a view containing your ViewModel that implements
|
|||
/// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
|
|||
/// <code>
|
|||
/// <![CDATA[
|
|||
/// <rxui:RoutedViewHost
|
|||
/// HorizontalAlignment="Stretch"
|
|||
/// VerticalAlignment="Stretch"
|
|||
/// Router="{Binding Router}">
|
|||
/// <rxui:RoutedViewHost.DefaultContent>
|
|||
/// <TextBlock Text="Default Content"/>
|
|||
/// </rxui:RoutedViewHost.DefaultContent>
|
|||
/// </rxui:RoutedViewHost>
|
|||
/// ]]>
|
|||
/// </code>
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// See <see href="https://reactiveui.net/docs/handbook/routing/">
|
|||
/// ReactiveUI routing documentation website</see> for more info.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger |
|||
{ |
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<RoutingState?> RouterProperty = |
|||
AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router)); |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string?> ViewContractProperty = |
|||
AvaloniaProperty.Register<RoutedViewHost, string?>(nameof(ViewContract)); |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<object?> DefaultContentProperty = |
|||
ViewModelViewHost.DefaultContentProperty.AddOwner<RoutedViewHost>(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
|
|||
/// </summary>
|
|||
public RoutedViewHost() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
var routerRemoved = this |
|||
.WhenAnyValue(x => x.Router) |
|||
.Where(router => router == null)! |
|||
.Cast<object?>(); |
|||
|
|||
var viewContract = this.WhenAnyValue(x => x.ViewContract); |
|||
|
|||
this.WhenAnyValue(x => x.Router) |
|||
.Where(router => router != null) |
|||
.SelectMany(router => router!.CurrentViewModel) |
|||
.Merge(routerRemoved) |
|||
.CombineLatest(viewContract) |
|||
.Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second)) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
|
|||
/// </summary>
|
|||
public RoutingState? Router |
|||
{ |
|||
get => GetValue(RouterProperty); |
|||
set => SetValue(RouterProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the view contract.
|
|||
/// </summary>
|
|||
public string? ViewContract |
|||
{ |
|||
get => GetValue(ViewContractProperty); |
|||
set => SetValue(ViewContractProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the content displayed whenever there is no page currently routed.
|
|||
/// </summary>
|
|||
public object? DefaultContent |
|||
{ |
|||
get => GetValue(DefaultContentProperty); |
|||
set => SetValue(DefaultContentProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ReactiveUI view locator used by this router.
|
|||
/// </summary>
|
|||
public IViewLocator? ViewLocator { get; set; } |
|||
|
|||
protected override Type StyleKeyOverride => typeof(TransitioningContentControl); |
|||
|
|||
/// <summary>
|
|||
/// Invoked when ReactiveUI router navigates to a view model.
|
|||
/// </summary>
|
|||
/// <param name="viewModel">ViewModel to which the user navigates.</param>
|
|||
/// <param name="contract">The contract for view resolution.</param>
|
|||
private void NavigateToViewModel(object? viewModel, string? contract) |
|||
{ |
|||
if (Router == null) |
|||
{ |
|||
this.Log().Warn("Router property is null. Falling back to default content."); |
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
if (viewModel == null) |
|||
{ |
|||
this.Log().Info("ViewModel is null. Falling back to default content."); |
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; |
|||
var viewInstance = viewLocator.ResolveView(viewModel, contract); |
|||
if (viewInstance == null) |
|||
{ |
|||
if (contract == null) |
|||
{ |
|||
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); |
|||
} |
|||
else |
|||
{ |
|||
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); |
|||
} |
|||
|
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
if (contract == null) |
|||
{ |
|||
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); |
|||
} |
|||
else |
|||
{ |
|||
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); |
|||
} |
|||
|
|||
viewInstance.ViewModel = viewModel; |
|||
if (viewInstance is IDataContextProvider provider) |
|||
provider.DataContext = viewModel; |
|||
Content = viewInstance; |
|||
} |
|||
} |
|||
} |
|||
@ -1,129 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// This content control will automatically load the View associated with
|
|||
/// the ViewModel property and display it. This control is very useful
|
|||
/// inside a DataTemplate to display the View associated with a ViewModel.
|
|||
/// </summary>
|
|||
public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger |
|||
{ |
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<object?> ViewModelProperty = |
|||
AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel)); |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string?> ViewContractProperty = |
|||
AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract)); |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<object?> DefaultContentProperty = |
|||
AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(DefaultContent)); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
|
|||
/// </summary>
|
|||
public ViewModelViewHost() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract) |
|||
.Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2)) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ViewModel to display.
|
|||
/// </summary>
|
|||
public object? ViewModel |
|||
{ |
|||
get => GetValue(ViewModelProperty); |
|||
set => SetValue(ViewModelProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the view contract.
|
|||
/// </summary>
|
|||
public string? ViewContract |
|||
{ |
|||
get => GetValue(ViewContractProperty); |
|||
set => SetValue(ViewContractProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the content displayed whenever there is no page currently routed.
|
|||
/// </summary>
|
|||
public object? DefaultContent |
|||
{ |
|||
get => GetValue(DefaultContentProperty); |
|||
set => SetValue(DefaultContentProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the view locator.
|
|||
/// </summary>
|
|||
public IViewLocator? ViewLocator { get; set; } |
|||
|
|||
protected override Type StyleKeyOverride => typeof(TransitioningContentControl); |
|||
|
|||
/// <summary>
|
|||
/// Invoked when ReactiveUI router navigates to a view model.
|
|||
/// </summary>
|
|||
/// <param name="viewModel">ViewModel to which the user navigates.</param>
|
|||
/// <param name="contract">The contract for view resolution.</param>
|
|||
private void NavigateToViewModel(object? viewModel, string? contract) |
|||
{ |
|||
if (viewModel == null) |
|||
{ |
|||
this.Log().Info("ViewModel is null. Falling back to default content."); |
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; |
|||
var viewInstance = viewLocator.ResolveView(viewModel, contract); |
|||
if (viewInstance == null) |
|||
{ |
|||
if (contract == null) |
|||
{ |
|||
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); |
|||
} |
|||
else |
|||
{ |
|||
this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); |
|||
} |
|||
|
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
if (contract == null) |
|||
{ |
|||
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); |
|||
} |
|||
else |
|||
{ |
|||
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); |
|||
} |
|||
|
|||
viewInstance.ViewModel = viewModel; |
|||
if (viewInstance is StyledElement styled) |
|||
styled.DataContext = viewModel; |
|||
Content = viewInstance; |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,8 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
|||
xmlns:rxui="http://reactiveui.net" |
|||
x:Class="Sample.App.AttachedProps" |
|||
Design.Width="300"> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</Window> |
|||
</Window> |
|||
|
|||
@ -1,10 +1,8 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
|||
xmlns:rxui="http://reactiveui.net" |
|||
x:Class="Sample.App.ControlWithoutWindow" |
|||
Design.Width="300"> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</UserControl> |
|||
</UserControl> |
|||
|
|||
@ -1,11 +1,11 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
|||
xmlns:custom="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.ColorPicker" |
|||
xmlns:controls="clr-namespace:Controls" |
|||
x:Class="Sample.App.CustomControls" |
|||
xmlns:rxui="http://reactiveui.net"> |
|||
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" /> |
|||
<rxui:RoutedViewHost Name="UriRoutedViewHost" /> |
|||
xmlns:av="https://github.com/avaloniaui"> |
|||
<custom:ColorPicker Name="ClrNamespaceColorPicker" /> |
|||
<av:ColorPicker Name="UriColorPicker" /> |
|||
<controls:CustomTextBox Name="UserNameTextBox" /> |
|||
<controls:EvilControl Name="EvilName" /> |
|||
</Window> |
|||
</Window> |
|||
|
|||
@ -1,8 +0,0 @@ |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
// Required to avoid InvalidOperationException sometimes thrown
|
|||
// from Splat.MemoizingMRUCache.cs which is not thread-safe.
|
|||
// Thrown when trying to access WhenActivated concurrently.
|
|||
[assembly: CollectionBehavior(DisableTestParallelization = true)] |
|||
[assembly: VerifyEmptyDispatcherAfterTest] |
|||
@ -1,146 +0,0 @@ |
|||
using Xunit; |
|||
using ReactiveUI; |
|||
using Avalonia.ReactiveUI; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using Avalonia.VisualTree; |
|||
using Avalonia.Controls.Presenters; |
|||
using Splat; |
|||
using System.Threading.Tasks; |
|||
using System; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class AutoDataTemplateBindingHookTest |
|||
{ |
|||
public class NestedViewModel : ReactiveObject { } |
|||
|
|||
public class NestedView : ReactiveUserControl<NestedViewModel> { } |
|||
|
|||
public class ExampleViewModel : ReactiveObject |
|||
{ |
|||
public ObservableCollection<NestedViewModel> Items { get; } = new ObservableCollection<NestedViewModel>(); |
|||
} |
|||
|
|||
public class ExampleView : ReactiveUserControl<ExampleViewModel> |
|||
{ |
|||
public ItemsControl List { get; } = new ItemsControl |
|||
{ |
|||
Template = GetTemplate() |
|||
}; |
|||
|
|||
public ExampleView(Action<ItemsControl> adjustItemsControl = null) |
|||
{ |
|||
adjustItemsControl?.Invoke(List); |
|||
List.ApplyTemplate(); |
|||
List.Presenter.ApplyTemplate(); |
|||
|
|||
Content = List; |
|||
ViewModel = new ExampleViewModel(); |
|||
this.OneWayBind(ViewModel, x => x.Items, x => x.List.ItemsSource); |
|||
} |
|||
} |
|||
|
|||
public AutoDataTemplateBindingHookTest() |
|||
{ |
|||
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor<NestedViewModel>)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() |
|||
{ |
|||
var view = new ExampleView(); |
|||
Assert.NotNull(view.List.ItemTemplate); |
|||
Assert.IsType<FuncDataTemplate<object>>(view.List.ItemTemplate); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Use_ViewModelViewHost_As_Data_Template_By_Default() |
|||
{ |
|||
var view = new ExampleView(); |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
Assert.IsType<ViewModelViewHost>(container.Child); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ViewModelViewHost_Should_Resolve_And_Embedd_Appropriate_View_Model() |
|||
{ |
|||
var view = new ExampleView(); |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
var host = (ViewModelViewHost) container.Child; |
|||
Assert.IsType<NestedViewModel>(host.ViewModel); |
|||
Assert.IsType<NestedViewModel>(host.DataContext); |
|||
|
|||
host.DataContext = "changed context"; |
|||
Assert.IsType<string>(host.ViewModel); |
|||
Assert.IsType<string>(host.DataContext); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Not_Override_Data_Template_Binding_When_Item_Template_Is_Set() |
|||
{ |
|||
var view = new ExampleView(control => control.ItemTemplate = GetItemTemplate()); |
|||
Assert.NotNull(view.List.ItemTemplate); |
|||
Assert.IsType<FuncDataTemplate<TextBlock>>(view.List.ItemTemplate); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Not_Use_View_Model_View_Host_When_Item_Template_Is_Set() |
|||
{ |
|||
var view = new ExampleView(control => control.ItemTemplate = GetItemTemplate()); |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
Assert.IsType<TextBlock>(container.Child); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Not_Use_View_Model_View_Host_When_Data_Templates_Are_Not_Empty() |
|||
{ |
|||
var view = new ExampleView(control => control.DataTemplates.Add(GetItemTemplate())); |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
Assert.IsType<TextBlock>(container.Child); |
|||
} |
|||
|
|||
private static FuncDataTemplate GetItemTemplate() |
|||
{ |
|||
return new FuncDataTemplate<TextBlock>((parent, scope) => new TextBlock()); |
|||
} |
|||
|
|||
private static FuncControlTemplate GetTemplate() |
|||
{ |
|||
return new FuncControlTemplate<ItemsControl>((parent, scope) => new Border |
|||
{ |
|||
Background = new Media.SolidColorBrush(0xffffffff), |
|||
Child = new ItemsPresenter |
|||
{ |
|||
Name = "PART_ItemsPresenter", |
|||
}.RegisterInNameScope(scope) |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -1,117 +0,0 @@ |
|||
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.IO; |
|||
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 Avalonia.Threading; |
|||
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()) |
|||
{ |
|||
var isLaunchingReceived = false; |
|||
var application = AvaloniaLocator.Current.GetRequiredService<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 AutoSuspendHelper_Should_Throw_When_Not_Supported_Lifetime_Is_Used() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
|||
using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) |
|||
{ |
|||
var application = AvaloniaLocator.Current.GetRequiredService<Application>(); |
|||
application.ApplicationLifetime = lifetime; |
|||
Assert.Throws<NotSupportedException>(() => new AutoSuspendHelper(application.ApplicationLifetime)); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void AutoSuspendHelper_Should_Throw_When_Lifetime_Is_Null() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
|||
{ |
|||
var application = AvaloniaLocator.Current.GetService<Application>(); |
|||
Assert.Throws<ArgumentNullException>(() => new AutoSuspendHelper(application.ApplicationLifetime)); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
|||
using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) |
|||
{ |
|||
var shouldPersistReceived = false; |
|||
var application = AvaloniaLocator.Current.GetRequiredService<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 FakeSuspensionDriver()); |
|||
suspension.OnFrameworkInitializationCompleted(); |
|||
|
|||
lifetime.Shutdown(); |
|||
|
|||
Assert.True(shouldPersistReceived); |
|||
Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState<AppState>().Example); |
|||
} |
|||
} |
|||
|
|||
private class FakeSuspensionDriver : ISuspensionDriver |
|||
{ |
|||
public IObservable<object> LoadState() => Observable.Empty<object>(); |
|||
|
|||
public IObservable<Unit> SaveState(object state) => Observable.Empty<Unit>(); |
|||
|
|||
public IObservable<Unit> InvalidateState() => Observable.Empty<Unit>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework> |
|||
<SignAssembly>false</SignAssembly> |
|||
</PropertyGroup> |
|||
<Import Project="..\..\build\UnitTests.NetCore.targets" /> |
|||
<Import Project="..\..\build\Moq.props" /> |
|||
<Import Project="..\..\build\XUnit.props" /> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" /> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -1,207 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Concurrency; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Platform; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia; |
|||
using ReactiveUI; |
|||
using DynamicData; |
|||
using Xunit; |
|||
using Splat; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.ReactiveUI; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class AvaloniaActivationForViewFetcherTest |
|||
{ |
|||
public class TestUserControl : UserControl, IActivatableView { } |
|||
|
|||
public class TestUserControlWithWhenActivated : UserControl, IActivatableView |
|||
{ |
|||
public bool Active { get; private set; } |
|||
|
|||
public TestUserControlWithWhenActivated() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
Active = true; |
|||
Disposable |
|||
.Create(() => Active = false) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public class TestWindowWithWhenActivated : Window, IActivatableView |
|||
{ |
|||
public bool Active { get; private set; } |
|||
|
|||
public TestWindowWithWhenActivated() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
Active = true; |
|||
Disposable |
|||
.Create(() => Active = false) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public class ActivatableViewModel : IActivatableViewModel |
|||
{ |
|||
public ViewModelActivator Activator { get; } |
|||
|
|||
public bool IsActivated { get; private set; } |
|||
|
|||
public ActivatableViewModel() |
|||
{ |
|||
Activator = new ViewModelActivator(); |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
IsActivated = true; |
|||
Disposable |
|||
.Create(() => IsActivated = false) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public class ActivatableWindow : ReactiveWindow<ActivatableViewModel> |
|||
{ |
|||
public ActivatableWindow() |
|||
{ |
|||
Content = new Border(); |
|||
this.WhenActivated(disposables => { }); |
|||
} |
|||
} |
|||
|
|||
public class ActivatableUserControl : ReactiveUserControl<ActivatableViewModel> |
|||
{ |
|||
public ActivatableUserControl() |
|||
{ |
|||
Content = new Border(); |
|||
this.WhenActivated(disposables => { }); |
|||
} |
|||
} |
|||
|
|||
public AvaloniaActivationForViewFetcherTest() => |
|||
Locator |
|||
.CurrentMutable |
|||
.RegisterConstant( |
|||
new AvaloniaActivationForViewFetcher(), |
|||
typeof(IActivationForViewFetcher)); |
|||
|
|||
[Fact] |
|||
public void Visual_Element_Is_Activated_And_Deactivated() |
|||
{ |
|||
var userControl = new TestUserControl(); |
|||
var activationForViewFetcher = new AvaloniaActivationForViewFetcher(); |
|||
|
|||
activationForViewFetcher |
|||
.GetActivationForView(userControl) |
|||
.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance) |
|||
.Bind(out var activated) |
|||
.Subscribe(); |
|||
|
|||
var fakeRenderedDecorator = new TestRoot(); |
|||
fakeRenderedDecorator.Child = userControl; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.True(activated[0]); |
|||
Assert.Equal(1, activated.Count); |
|||
|
|||
fakeRenderedDecorator.Child = null; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.True(activated[0]); |
|||
Assert.False(activated[1]); |
|||
Assert.Equal(2, activated.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Get_Affinity_For_View_Should_Return_Non_Zero_For_Visual_Elements() |
|||
{ |
|||
var userControl = new TestUserControl(); |
|||
var activationForViewFetcher = new AvaloniaActivationForViewFetcher(); |
|||
|
|||
var forUserControl = activationForViewFetcher.GetAffinityForView(userControl.GetType()); |
|||
var forNonUserControl = activationForViewFetcher.GetAffinityForView(typeof(object)); |
|||
|
|||
Assert.NotEqual(0, forUserControl); |
|||
Assert.Equal(0, forNonUserControl); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activation_For_View_Fetcher_Should_Support_When_Activated() |
|||
{ |
|||
var userControl = new TestUserControlWithWhenActivated(); |
|||
Assert.False(userControl.Active); |
|||
|
|||
var fakeRenderedDecorator = new TestRoot(); |
|||
fakeRenderedDecorator.Child = userControl; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.True(userControl.Active); |
|||
|
|||
fakeRenderedDecorator.Child = null; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.False(userControl.Active); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activation_For_View_Fetcher_Should_Support_Windows() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var window = new TestWindowWithWhenActivated(); |
|||
Assert.False(window.Active); |
|||
|
|||
window.Show(); |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.True(window.Active); |
|||
|
|||
window.Close(); |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.False(window.Active); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activatable_Window_View_Model_Is_Activated_And_Deactivated() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var viewModel = new ActivatableViewModel(); |
|||
var window = new ActivatableWindow { ViewModel = viewModel }; |
|||
Assert.False(viewModel.IsActivated); |
|||
|
|||
window.Show(); |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.True(viewModel.IsActivated); |
|||
|
|||
window.Close(); |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.False(viewModel.IsActivated); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activatable_User_Control_View_Model_Is_Activated_And_Deactivated() |
|||
{ |
|||
var root = new TestRoot(); |
|||
var viewModel = new ActivatableViewModel(); |
|||
var control = new ActivatableUserControl { ViewModel = viewModel }; |
|||
Assert.False(viewModel.IsActivated); |
|||
|
|||
root.Child = control; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.True(viewModel.IsActivated); |
|||
|
|||
root.Child = null; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.False(viewModel.IsActivated); |
|||
} |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class AvaloniaObjectTests_GetSubject |
|||
{ |
|||
[Fact] |
|||
public void GetSubject_Returns_Values() |
|||
{ |
|||
var source = new Class1 { Foo = "foo" }; |
|||
var target = source.GetSubject(Class1.FooProperty); |
|||
var result = new List<string>(); |
|||
|
|||
target.Subscribe(x => result.Add(x)); |
|||
source.Foo = "bar"; |
|||
source.Foo = "baz"; |
|||
|
|||
Assert.Equal(new[] { "foo", "bar", "baz" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetSubject_Sets_Values() |
|||
{ |
|||
var source = new Class1 { Foo = "foo" }; |
|||
var target = source.GetSubject(Class1.FooProperty); |
|||
|
|||
target.OnNext("bar"); |
|||
Assert.Equal("bar", source.Foo); |
|||
} |
|||
|
|||
private class Class1 : AvaloniaObject |
|||
{ |
|||
public static readonly StyledProperty<string> FooProperty = |
|||
AvaloniaProperty.Register<Class1, string>(nameof(Foo), "foodefault"); |
|||
|
|||
public string Foo |
|||
{ |
|||
get => GetValue(FooProperty); |
|||
set => SetValue(FooProperty, value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,196 +0,0 @@ |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ReactiveUserControlTest : ScopedTestBase |
|||
{ |
|||
public class ExampleViewModel : ReactiveObject, IActivatableViewModel |
|||
{ |
|||
public bool IsActive { get; private set; } |
|||
|
|||
public ViewModelActivator Activator { get; } = new ViewModelActivator(); |
|||
|
|||
public ExampleViewModel() => this.WhenActivated(disposables => |
|||
{ |
|||
IsActive = true; |
|||
Disposable |
|||
.Create(() => IsActive = false) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
|
|||
public class ExampleView : ReactiveUserControl<ExampleViewModel> { } |
|||
|
|||
public ReactiveUserControlTest() => |
|||
Locator |
|||
.CurrentMutable |
|||
.RegisterConstant( |
|||
new AvaloniaActivationForViewFetcher(), |
|||
typeof(IActivationForViewFetcher)); |
|||
|
|||
[Fact] |
|||
public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() |
|||
{ |
|||
var root = new TestRoot(); |
|||
var view = new ExampleView(); |
|||
root.Child = view; |
|||
|
|||
var viewModel = new ExampleViewModel(); |
|||
Assert.Null(view.ViewModel); |
|||
|
|||
view.DataContext = viewModel; |
|||
Assert.Equal(view.ViewModel, viewModel); |
|||
Assert.Equal(view.DataContext, viewModel); |
|||
|
|||
view.DataContext = null; |
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
|
|||
view.ViewModel = viewModel; |
|||
Assert.Equal(viewModel, view.ViewModel); |
|||
Assert.Equal(viewModel, view.DataContext); |
|||
|
|||
view.ViewModel = null; |
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Start_With_NotNull_Activated_ViewModel() |
|||
{ |
|||
var root = new TestRoot(); |
|||
var view = new ExampleView {ViewModel = new ExampleViewModel()}; |
|||
|
|||
Assert.False(view.ViewModel.IsActive); |
|||
|
|||
root.Child = view; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
|
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.True(view.ViewModel.IsActive); |
|||
|
|||
root.Child = null; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
|
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.False(view.ViewModel.IsActive); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Start_With_NotNull_Activated_DataContext() |
|||
{ |
|||
var root = new TestRoot(); |
|||
var view = new ExampleView {DataContext = new ExampleViewModel()}; |
|||
|
|||
Assert.False(view.ViewModel.IsActive); |
|||
|
|||
root.Child = view; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
|
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.True(view.ViewModel.IsActive); |
|||
|
|||
root.Child = null; |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
|
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.False(view.ViewModel.IsActive); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Inherit_DataContext() |
|||
{ |
|||
var vm1 = new ExampleViewModel(); |
|||
var vm2 = new ExampleViewModel(); |
|||
var view = new ExampleView(); |
|||
var root = new TestRoot(view); |
|||
|
|||
Assert.Null(view.DataContext); |
|||
Assert.Null(view.ViewModel); |
|||
|
|||
root.DataContext = vm1; |
|||
|
|||
Assert.Same(vm1, view.DataContext); |
|||
Assert.Same(vm1, view.ViewModel); |
|||
|
|||
root.DataContext = null; |
|||
|
|||
Assert.Null(view.DataContext); |
|||
Assert.Null(view.ViewModel); |
|||
|
|||
root.DataContext = vm2; |
|||
|
|||
Assert.Same(vm2, view.DataContext); |
|||
Assert.Same(vm2, view.ViewModel); |
|||
} |
|||
|
|||
// https://github.com/AvaloniaUI/Avalonia/issues/15060
|
|||
[Fact] |
|||
public void Should_Not_Inherit_DataContext_Of_Wrong_Type() |
|||
{ |
|||
var view = new ExampleView(); |
|||
var root = new TestRoot(view); |
|||
|
|||
Assert.Null(view.DataContext); |
|||
Assert.Null(view.ViewModel); |
|||
|
|||
root.DataContext = this; |
|||
|
|||
Assert.Same(this, view.DataContext); |
|||
Assert.Null(view.ViewModel); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Not_Overlap_Change_Notifications() |
|||
{ |
|||
var vm1 = new ExampleViewModel(); |
|||
var vm2 = new ExampleViewModel(); |
|||
|
|||
var view1 = new ExampleView(); |
|||
var view2 = new ExampleView(); |
|||
|
|||
Assert.Null(view1.DataContext); |
|||
Assert.Null(view2.DataContext); |
|||
Assert.Null(view1.ViewModel); |
|||
Assert.Null(view2.ViewModel); |
|||
|
|||
view1.DataContext = vm1; |
|||
|
|||
Assert.Same(vm1, view1.DataContext); |
|||
Assert.Same(vm1, view1.ViewModel); |
|||
Assert.Null(view2.DataContext); |
|||
Assert.Null(view2.ViewModel); |
|||
|
|||
view2.DataContext = vm2; |
|||
|
|||
Assert.Same(vm1, view1.DataContext); |
|||
Assert.Same(vm1, view1.ViewModel); |
|||
Assert.Same(vm2, view2.DataContext); |
|||
Assert.Same(vm2, view2.ViewModel); |
|||
|
|||
view1.ViewModel = null; |
|||
|
|||
Assert.Null(view1.DataContext); |
|||
Assert.Null(view1.ViewModel); |
|||
Assert.Same(vm2, view2.DataContext); |
|||
Assert.Same(vm2, view2.ViewModel); |
|||
|
|||
view2.ViewModel = null; |
|||
|
|||
Assert.Null(view1.DataContext); |
|||
Assert.Null(view2.DataContext); |
|||
Assert.Null(view1.ViewModel); |
|||
Assert.Null(view2.ViewModel); |
|||
} |
|||
} |
|||
} |
|||
@ -1,116 +0,0 @@ |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ReactiveWindowTest |
|||
{ |
|||
public class ExampleViewModel : ReactiveObject, IActivatableViewModel |
|||
{ |
|||
public bool IsActive { get; private set; } |
|||
|
|||
public ViewModelActivator Activator { get; } = new ViewModelActivator(); |
|||
|
|||
public ExampleViewModel() => this.WhenActivated(disposables => |
|||
{ |
|||
IsActive = true; |
|||
Disposable |
|||
.Create(() => IsActive = false) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
|
|||
public class ExampleWindow : ReactiveWindow<ExampleViewModel> { } |
|||
|
|||
public ReactiveWindowTest() => |
|||
Locator |
|||
.CurrentMutable |
|||
.RegisterConstant( |
|||
new AvaloniaActivationForViewFetcher(), |
|||
typeof(IActivationForViewFetcher)); |
|||
|
|||
[Fact] |
|||
public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var view = new ExampleWindow(); |
|||
var viewModel = new ExampleViewModel(); |
|||
view.Show(); |
|||
|
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
|
|||
view.DataContext = viewModel; |
|||
Assert.Equal(viewModel, view.ViewModel); |
|||
Assert.Equal(viewModel, view.DataContext); |
|||
|
|||
view.DataContext = null; |
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
|
|||
view.ViewModel = viewModel; |
|||
Assert.Equal(viewModel, view.ViewModel); |
|||
Assert.Equal(viewModel, view.DataContext); |
|||
|
|||
view.ViewModel = null; |
|||
Assert.Null(view.ViewModel); |
|||
Assert.Null(view.DataContext); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Start_With_NotNull_Activated_ViewModel() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var view = new ExampleWindow { ViewModel = new ExampleViewModel() }; |
|||
|
|||
Assert.False(view.ViewModel.IsActive); |
|||
|
|||
view.Show(); |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
|
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.True(view.ViewModel.IsActive); |
|||
|
|||
view.Close(); |
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
|
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.False(view.ViewModel.IsActive); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Start_With_NotNull_Activated_DataContext() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var view = new ExampleWindow { DataContext = new ExampleViewModel() }; |
|||
|
|||
Assert.False(view.ViewModel.IsActive); |
|||
|
|||
view.Show(); |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.True(view.ViewModel.IsActive); |
|||
|
|||
view.Close(); |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(view.ViewModel); |
|||
Assert.NotNull(view.DataContext); |
|||
Assert.False(view.ViewModel.IsActive); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,223 +0,0 @@ |
|||
using System; |
|||
using System.Reactive.Concurrency; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Platform; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia; |
|||
using ReactiveUI; |
|||
using DynamicData; |
|||
using Xunit; |
|||
using Splat; |
|||
using Avalonia.Markup.Xaml; |
|||
using System.ComponentModel; |
|||
using System.Threading.Tasks; |
|||
using System.Reactive; |
|||
using Avalonia.ReactiveUI; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class RoutedViewHostTest |
|||
{ |
|||
public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public string UrlPathSegment => "first"; |
|||
|
|||
public IScreen HostScreen { get; set; } |
|||
} |
|||
|
|||
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { } |
|||
|
|||
public class AlternativeFirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { } |
|||
|
|||
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public string UrlPathSegment => "second"; |
|||
|
|||
public IScreen HostScreen { get; set; } |
|||
} |
|||
|
|||
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { } |
|||
|
|||
public class AlternativeSecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { } |
|||
|
|||
public class ScreenViewModel : ReactiveObject, IScreen |
|||
{ |
|||
public RoutingState Router { get; } = new RoutingState(); |
|||
} |
|||
|
|||
public static string AlternativeViewContract => "AlternativeView"; |
|||
|
|||
public RoutedViewHostTest() |
|||
{ |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new AlternativeFirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>), AlternativeViewContract); |
|||
Locator.CurrentMutable.Register(() => new AlternativeSecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>), AlternativeViewContract); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState() |
|||
{ |
|||
var screen = new ScreenViewModel(); |
|||
var defaultContent = new TextBlock(); |
|||
var host = new RoutedViewHost |
|||
{ |
|||
Router = screen.Router, |
|||
DefaultContent = defaultContent, |
|||
PageTransition = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<TextBlock>(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
var first = new FirstRoutableViewModel(); |
|||
screen.Router.Navigate.Execute(first).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<FirstRoutableView>(host.Content); |
|||
Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel); |
|||
|
|||
var second = new SecondRoutableViewModel(); |
|||
screen.Router.Navigate.Execute(second).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<SecondRoutableView>(host.Content); |
|||
Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext); |
|||
Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel); |
|||
|
|||
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<FirstRoutableView>(host.Content); |
|||
Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel); |
|||
|
|||
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<TextBlock>(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState_And_Contract() |
|||
{ |
|||
var screen = new ScreenViewModel(); |
|||
var defaultContent = new TextBlock(); |
|||
var host = new RoutedViewHost |
|||
{ |
|||
Router = screen.Router, |
|||
DefaultContent = defaultContent, |
|||
PageTransition = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<TextBlock>(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
var first = new FirstRoutableViewModel(); |
|||
screen.Router.Navigate.Execute(first).Subscribe(); |
|||
|
|||
host.ViewContract = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<FirstRoutableView>(host.Content); |
|||
Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel); |
|||
|
|||
host.ViewContract = AlternativeViewContract; |
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<AlternativeFirstRoutableView>(host.Content); |
|||
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext); |
|||
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel); |
|||
|
|||
var second = new SecondRoutableViewModel(); |
|||
screen.Router.Navigate.Execute(second).Subscribe(); |
|||
|
|||
host.ViewContract = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<SecondRoutableView>(host.Content); |
|||
Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext); |
|||
Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel); |
|||
|
|||
host.ViewContract = AlternativeViewContract; |
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<AlternativeSecondRoutableView>(host.Content); |
|||
Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).DataContext); |
|||
Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).ViewModel); |
|||
|
|||
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<AlternativeFirstRoutableView>(host.Content); |
|||
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext); |
|||
Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel); |
|||
|
|||
screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<TextBlock>(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RoutedViewHost_Should_Show_Default_Content_When_Router_Is_Null() |
|||
{ |
|||
var screen = new ScreenViewModel(); |
|||
var defaultContent = new TextBlock(); |
|||
var host = new RoutedViewHost |
|||
{ |
|||
DefaultContent = defaultContent, |
|||
PageTransition = null, |
|||
Router = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
host.Router = screen.Router; |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
var first = new FirstRoutableViewModel(); |
|||
screen.Router.Navigate.Execute(first).Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<FirstRoutableView>(host.Content); |
|||
|
|||
host.Router = null; |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
host.Router = screen.Router; |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.IsType<FirstRoutableView>(host.Content); |
|||
} |
|||
} |
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.VisualTree; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class TransitioningContentControlTest |
|||
{ |
|||
[Fact] |
|||
public void Transitioning_Control_Template_Should_Be_Instantiated() |
|||
{ |
|||
var target = new TransitioningContentControl |
|||
{ |
|||
PageTransition = null, |
|||
Template = GetTemplate(), |
|||
Content = "Foo" |
|||
}; |
|||
target.ApplyTemplate(); |
|||
target.Presenter.UpdateChild(); |
|||
|
|||
var child = ((Visual)target).GetVisualChildren().Single(); |
|||
Assert.IsType<Border>(child); |
|||
child = child.GetVisualChildren().Single(); |
|||
Assert.IsType<ContentPresenter>(child); |
|||
child = child.GetVisualChildren().Single(); |
|||
Assert.IsType<TextBlock>(child); |
|||
} |
|||
|
|||
private static FuncControlTemplate GetTemplate() |
|||
{ |
|||
return new FuncControlTemplate<ContentControl>((parent, scope) => |
|||
{ |
|||
return new Border |
|||
{ |
|||
Background = new Media.SolidColorBrush(0xffffffff), |
|||
Child = new ContentPresenter |
|||
{ |
|||
Name = "PART_ContentPresenter", |
|||
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], |
|||
[~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], |
|||
}.RegisterInNameScope(scope) |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -1,144 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ViewModelViewHostTest |
|||
{ |
|||
public class FirstViewModel { } |
|||
|
|||
public class FirstView : ReactiveUserControl<FirstViewModel> { } |
|||
|
|||
public class AlternativeFirstView : ReactiveUserControl<FirstViewModel> { } |
|||
|
|||
public class SecondViewModel : ReactiveObject { } |
|||
|
|||
public class SecondView : ReactiveUserControl<SecondViewModel> { } |
|||
|
|||
public class AlternativeSecondView : ReactiveUserControl<SecondViewModel> { } |
|||
|
|||
public static string AlternativeViewContract => "AlternativeView"; |
|||
|
|||
public ViewModelViewHostTest() |
|||
{ |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor<SecondViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new AlternativeFirstView(), typeof(IViewFor<FirstViewModel>), AlternativeViewContract); |
|||
Locator.CurrentMutable.Register(() => new AlternativeSecondView(), typeof(IViewFor<SecondViewModel>), AlternativeViewContract); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel() |
|||
{ |
|||
var defaultContent = new TextBlock(); |
|||
var host = new ViewModelViewHost |
|||
{ |
|||
DefaultContent = defaultContent, |
|||
PageTransition = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
var first = new FirstViewModel(); |
|||
host.ViewModel = first; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstView), host.Content.GetType()); |
|||
Assert.Equal(first, ((FirstView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstView)host.Content).ViewModel); |
|||
|
|||
var second = new SecondViewModel(); |
|||
host.ViewModel = second; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(SecondView), host.Content.GetType()); |
|||
Assert.Equal(second, ((SecondView)host.Content).DataContext); |
|||
Assert.Equal(second, ((SecondView)host.Content).ViewModel); |
|||
|
|||
host.ViewModel = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
host.ViewModel = first; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstView), host.Content.GetType()); |
|||
Assert.Equal(first, ((FirstView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstView)host.Content).ViewModel); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel_And_Contract() |
|||
{ |
|||
var defaultContent = new TextBlock(); |
|||
var host = new ViewModelViewHost |
|||
{ |
|||
DefaultContent = defaultContent, |
|||
PageTransition = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
var first = new FirstViewModel(); |
|||
host.ViewModel = first; |
|||
|
|||
host.ViewContract = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstView), host.Content.GetType()); |
|||
Assert.Equal(first, ((FirstView)host.Content).DataContext); |
|||
Assert.Equal(first, ((FirstView)host.Content).ViewModel); |
|||
|
|||
host.ViewContract = AlternativeViewContract; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(AlternativeFirstView), host.Content.GetType()); |
|||
Assert.Equal(first, ((AlternativeFirstView)host.Content).DataContext); |
|||
Assert.Equal(first, ((AlternativeFirstView)host.Content).ViewModel); |
|||
|
|||
var second = new SecondViewModel(); |
|||
host.ViewModel = second; |
|||
|
|||
host.ViewContract = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(SecondView), host.Content.GetType()); |
|||
Assert.Equal(second, ((SecondView)host.Content).DataContext); |
|||
Assert.Equal(second, ((SecondView)host.Content).ViewModel); |
|||
|
|||
host.ViewContract = AlternativeViewContract; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(AlternativeSecondView), host.Content.GetType()); |
|||
Assert.Equal(second, ((AlternativeSecondView)host.Content).DataContext); |
|||
Assert.Equal(second, ((AlternativeSecondView)host.Content).ViewModel); |
|||
|
|||
host.ViewModel = null; |
|||
|
|||
host.ViewContract = null; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
host.ViewContract = AlternativeViewContract; |
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue