55 changed files with 115 additions and 2613 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" |
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="using:Generators.Sandbox.Controls" |
xmlns:controls="using:Generators.Sandbox.Controls" |
||||
x:Class="Generators.Sandbox.Controls.SignUpView"> |
xmlns:vm="clr-namespace:Generators.Sandbox.ViewModels" |
||||
<StackPanel> |
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" |
<controls:CustomTextBox Margin="0 10 0 0" |
||||
x:Name="UserNameTextBox" |
x:Name="UserNameTextBox" |
||||
|
Text="{Binding UserName}" |
||||
Watermark="Please, enter user name..." |
Watermark="Please, enter user name..." |
||||
UseFloatingWatermark="True" /> |
UseFloatingWatermark="True" /> |
||||
<TextBlock x:Name="UserNameValidation" |
<TextBlock x:Name="UserNameValidation" |
||||
|
Text="{Binding UserNameValidation}" |
||||
Foreground="Red" |
Foreground="Red" |
||||
FontSize="12" /> |
FontSize="12" /> |
||||
<TextBox Margin="0 10 0 0" |
<TextBox Margin="0 10 0 0" |
||||
x:Name="PasswordTextBox" |
x:Name="PasswordTextBox" |
||||
|
Text="{Binding Password}" |
||||
Watermark="Please, enter your password..." |
Watermark="Please, enter your password..." |
||||
UseFloatingWatermark="True" |
UseFloatingWatermark="True" |
||||
PasswordChar="*" /> |
PasswordChar="*" /> |
||||
<TextBlock x:Name="PasswordValidation" |
<TextBlock x:Name="PasswordValidation" |
||||
|
Text="{Binding PasswordValidation}" |
||||
Foreground="Red" |
Foreground="Red" |
||||
FontSize="12" /> |
FontSize="12" /> |
||||
<TextBox Margin="0 10 0 0" |
<TextBox Margin="0 10 0 0" |
||||
x:Name="ConfirmPasswordTextBox" |
x:Name="ConfirmPasswordTextBox" |
||||
|
Text="{Binding ConfirmPassword}" |
||||
Watermark="Please, confirm the password..." |
Watermark="Please, confirm the password..." |
||||
UseFloatingWatermark="True" |
UseFloatingWatermark="True" |
||||
PasswordChar="*" /> |
PasswordChar="*" /> |
||||
<TextBlock x:Name="ConfirmPasswordValidation" |
<TextBlock x:Name="ConfirmPasswordValidation" |
||||
|
Text="{Binding ConfirmPasswordValidation}" |
||||
TextWrapping="Wrap" |
TextWrapping="Wrap" |
||||
Foreground="Red" |
Foreground="Red" |
||||
FontSize="12" /> |
FontSize="12" /> |
||||
<TextBlock> |
<TextBlock Text="Press the button below to sign up." /> |
||||
<TextBlock.Inlines> |
|
||||
<InlineCollection> |
|
||||
<Run x:Name="SignUpButtonDescription" /> |
|
||||
</InlineCollection> |
|
||||
</TextBlock.Inlines> |
|
||||
</TextBlock> |
|
||||
<Button Margin="0 10 0 5" |
<Button Margin="0 10 0 5" |
||||
Content="Sign up" |
Content="Sign up" |
||||
|
Command="{Binding SignUp}" |
||||
x:Name="SignUpButton" /> |
x:Name="SignUpButton" /> |
||||
<TextBlock x:Name="CompoundValidation" |
<TextBlock x:Name="CompoundValidation" |
||||
TextWrapping="Wrap" |
TextWrapping="Wrap" |
||||
Foreground="Red" |
Foreground="Red" |
||||
FontSize="12" /> |
FontSize="12" |
||||
|
Text="{Binding CompoundValidation}" /> |
||||
</StackPanel> |
</StackPanel> |
||||
</UserControl> |
</UserControl> |
||||
|
|||||
@ -1,70 +1,81 @@ |
|||||
using System.Reactive; |
using System; |
||||
using ReactiveUI; |
using System.ComponentModel; |
||||
using ReactiveUI.Validation.Extensions; |
using System.ComponentModel.DataAnnotations; |
||||
using ReactiveUI.Validation.Helpers; |
using System.Linq; |
||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||
|
using CommunityToolkit.Mvvm.Input; |
||||
|
|
||||
namespace Generators.Sandbox.ViewModels; |
namespace Generators.Sandbox.ViewModels; |
||||
|
|
||||
public class SignUpViewModel : ReactiveValidationObject |
public class SignUpViewModel : ObservableValidator |
||||
{ |
{ |
||||
private string _userName = string.Empty; |
private string? _userName; |
||||
private string _password = string.Empty; |
private string? _password; |
||||
private string _confirmPassword = string.Empty; |
private string? _confirmPassword; |
||||
|
|
||||
public SignUpViewModel() |
public SignUpViewModel() |
||||
{ |
{ |
||||
this.ValidationRule( |
UserName = "Joseph!"; |
||||
vm => vm.UserName, |
Password = "1234"; |
||||
name => !string.IsNullOrWhiteSpace(name), |
ConfirmPassword = "1234"; |
||||
"UserName is required."); |
SignUp = new RelayCommand(() => { }, () => !HasErrors); |
||||
|
|
||||
this.ValidationRule( |
ErrorsChanged += OnErrorsChanged; |
||||
vm => vm.Password, |
} |
||||
password => !string.IsNullOrWhiteSpace(password), |
|
||||
"Password is required."); |
|
||||
|
|
||||
this.ValidationRule( |
|
||||
vm => vm.Password, |
|
||||
password => password?.Length > 2, |
|
||||
password => $"Password should be longer, current length: {password.Length}"); |
|
||||
|
|
||||
this.ValidationRule( |
public RelayCommand SignUp { get; } |
||||
vm => vm.ConfirmPassword, |
|
||||
confirmation => !string.IsNullOrWhiteSpace(confirmation), |
|
||||
"Confirm password field is required."); |
|
||||
|
|
||||
var passwordsObservable = |
[Required] |
||||
this.WhenAnyValue( |
public string? UserName |
||||
x => x.Password, |
{ |
||||
x => x.ConfirmPassword, |
get => _userName; |
||||
(password, confirmation) => |
set => SetProperty(ref _userName, value, validate: true); |
||||
password == confirmation); |
} |
||||
|
|
||||
this.ValidationRule( |
public string? UserNameValidation |
||||
vm => vm.ConfirmPassword, |
=> GetValidationMessage(nameof(UserName)); |
||||
passwordsObservable, |
|
||||
"Passwords must match."); |
|
||||
|
|
||||
SignUp = ReactiveCommand.Create(() => {}, this.IsValid()); |
[Required] |
||||
|
[MinLength(2)] |
||||
|
public string? Password |
||||
|
{ |
||||
|
get => _password; |
||||
|
set |
||||
|
{ |
||||
|
if (SetProperty(ref _password, 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; |
get => _confirmPassword; |
||||
set => this.RaiseAndSetIfChanged(ref _userName, value); |
set => SetProperty(ref _confirmPassword, 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; |
if (e.PropertyName is not null) |
||||
set => this.RaiseAndSetIfChanged(ref _password, value); |
OnPropertyChanged(e.PropertyName + "Validation"); |
||||
|
|
||||
|
OnPropertyChanged(CompoundValidation); |
||||
|
SignUp.NotifyCanExecuteChanged(); |
||||
} |
} |
||||
|
|
||||
public string ConfirmPassword |
private string? GetValidationMessage(string? propertyName) |
||||
{ |
{ |
||||
get => _confirmPassword; |
var message = string.Join(Environment.NewLine, GetErrors(propertyName).Select(v => v.ErrorMessage)); |
||||
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value); |
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" |
<Window xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
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" |
x:Class="Sample.App.AttachedProps" |
||||
Design.Width="300"> |
Design.Width="300"> |
||||
<TextBox Name="UserNameTextBox" |
<TextBox Name="UserNameTextBox" |
||||
Watermark="Username input" |
Watermark="Username input" |
||||
UseFloatingWatermark="True" /> |
UseFloatingWatermark="True" /> |
||||
</Window> |
</Window> |
||||
|
|||||
@ -1,10 +1,8 @@ |
|||||
<UserControl xmlns="https://github.com/avaloniaui" |
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
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" |
x:Class="Sample.App.ControlWithoutWindow" |
||||
Design.Width="300"> |
Design.Width="300"> |
||||
<TextBox Name="UserNameTextBox" |
<TextBox Name="UserNameTextBox" |
||||
Watermark="Username input" |
Watermark="Username input" |
||||
UseFloatingWatermark="True" /> |
UseFloatingWatermark="True" /> |
||||
</UserControl> |
</UserControl> |
||||
|
|||||
@ -1,11 +1,11 @@ |
|||||
<Window xmlns="https://github.com/avaloniaui" |
<Window xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
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" |
xmlns:controls="clr-namespace:Controls" |
||||
x:Class="Sample.App.CustomControls" |
x:Class="Sample.App.CustomControls" |
||||
xmlns:rxui="http://reactiveui.net"> |
xmlns:av="https://github.com/avaloniaui"> |
||||
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" /> |
<custom:ColorPicker Name="ClrNamespaceColorPicker" /> |
||||
<rxui:RoutedViewHost Name="UriRoutedViewHost" /> |
<av:ColorPicker Name="UriColorPicker" /> |
||||
<controls:CustomTextBox Name="UserNameTextBox" /> |
<controls:CustomTextBox Name="UserNameTextBox" /> |
||||
<controls:EvilControl Name="EvilName" /> |
<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