Browse Source

Merge pull request #4917 from worldbeater/sync-vm-and-datacontext

rxui: TwoWay DataContext and ViewModel Synchronization
pull/4934/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
fe795042ef
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      src/Avalonia.ReactiveUI/ReactiveUserControl.cs
  2. 18
      src/Avalonia.ReactiveUI/ReactiveWindow.cs
  3. 34
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
  4. 79
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
  5. 87
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs

18
src/Avalonia.ReactiveUI/ReactiveUserControl.cs

@ -1,3 +1,6 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia; using Avalonia;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Controls; using Avalonia.Controls;
@ -6,8 +9,10 @@ using ReactiveUI;
namespace Avalonia.ReactiveUI namespace Avalonia.ReactiveUI
{ {
/// <summary> /// <summary>
/// A ReactiveUI UserControl that implements <see cref="IViewFor{TViewModel}"/> /// A ReactiveUI <see cref="UserControl"/> that implements the <see cref="IViewFor{TViewModel}"/> interface and
/// and will activate your ViewModel automatically if it supports activation. /// 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> /// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam> /// <typeparam name="TViewModel">ViewModel type.</typeparam>
public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class
@ -20,7 +25,14 @@ namespace Avalonia.ReactiveUI
/// </summary> /// </summary>
public ReactiveUserControl() public ReactiveUserControl()
{ {
DataContextChanged += (sender, args) => ViewModel = DataContext as TViewModel; // This WhenActivated block calls ViewModel's WhenActivated
// block if the ViewModel implements IActivatableViewModel.
this.WhenActivated(disposables => { });
this.ObservableForProperty(x => x.ViewModel, false, true)
.Subscribe(args => DataContext = args.Value);
this.ObservableForProperty(x => x.DataContext, false, true)
.Subscribe(args => ViewModel = args.Value as TViewModel);
} }
/// <summary> /// <summary>

18
src/Avalonia.ReactiveUI/ReactiveWindow.cs

@ -1,3 +1,6 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia; using Avalonia;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Controls; using Avalonia.Controls;
@ -6,8 +9,10 @@ using ReactiveUI;
namespace Avalonia.ReactiveUI namespace Avalonia.ReactiveUI
{ {
/// <summary> /// <summary>
/// A ReactiveUI Window that implements <see cref="IViewFor{TViewModel}"/> /// A ReactiveUI <see cref="Window"/> that implements the <see cref="IViewFor{TViewModel}"/> interface and will
/// and will activate your ViewModel automatically if it supports activation. /// 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> /// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam> /// <typeparam name="TViewModel">ViewModel type.</typeparam>
public class ReactiveWindow<TViewModel> : Window, IViewFor<TViewModel> where TViewModel : class public class ReactiveWindow<TViewModel> : Window, IViewFor<TViewModel> where TViewModel : class
@ -20,7 +25,14 @@ namespace Avalonia.ReactiveUI
/// </summary> /// </summary>
public ReactiveWindow() public ReactiveWindow()
{ {
DataContextChanged += (sender, args) => ViewModel = DataContext as TViewModel; // This WhenActivated block calls ViewModel's WhenActivated
// block if the ViewModel implements IActivatableViewModel.
this.WhenActivated(disposables => { });
this.ObservableForProperty(x => x.ViewModel, false, true)
.Subscribe(args => DataContext = args.Value);
this.ObservableForProperty(x => x.DataContext, false, true)
.Subscribe(args => ViewModel = args.Value as TViewModel);
} }
/// <summary> /// <summary>

34
tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

@ -74,44 +74,26 @@ namespace Avalonia.ReactiveUI.UnitTests
{ {
public ActivatableWindow() public ActivatableWindow()
{ {
InitializeComponent(); Content = new Border();
Assert.IsType<Border>(Content);
this.WhenActivated(disposables => { }); this.WhenActivated(disposables => { });
} }
private void InitializeComponent()
{
AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'>
<Border/>
</Window>", null, this);
}
} }
public class ActivatableUserControl : ReactiveUserControl<ActivatableViewModel> public class ActivatableUserControl : ReactiveUserControl<ActivatableViewModel>
{ {
public ActivatableUserControl() public ActivatableUserControl()
{ {
InitializeComponent(); Content = new Border();
Assert.IsType<Border>(Content);
this.WhenActivated(disposables => { }); this.WhenActivated(disposables => { });
} }
private void InitializeComponent()
{
AvaloniaRuntimeXamlLoader.Load(@"
<UserControl xmlns='https://github.com/avaloniaui'>
<Border/>
</UserControl>", null, this);
}
} }
public AvaloniaActivationForViewFetcherTest() public AvaloniaActivationForViewFetcherTest() =>
{ Locator
Locator.CurrentMutable.RegisterConstant( .CurrentMutable
new AvaloniaActivationForViewFetcher(), .RegisterConstant(
typeof(IActivationForViewFetcher)); new AvaloniaActivationForViewFetcher(),
} typeof(IActivationForViewFetcher));
[Fact] [Fact]
public void Visual_Element_Is_Activated_And_Deactivated() public void Visual_Element_Is_Activated_And_Deactivated()

79
tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs

@ -1,4 +1,4 @@
using Avalonia.Controls; using System.Reactive.Disposables;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using ReactiveUI; using ReactiveUI;
using Splat; using Splat;
@ -8,14 +8,37 @@ namespace Avalonia.ReactiveUI.UnitTests
{ {
public class ReactiveUserControlTest public class ReactiveUserControlTest
{ {
public class ExampleViewModel : ReactiveObject { } 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 class ExampleView : ReactiveUserControl<ExampleViewModel> { }
public ReactiveUserControlTest() =>
Locator
.CurrentMutable
.RegisterConstant(
new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
[Fact] [Fact]
public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model()
{ {
var root = new TestRoot();
var view = new ExampleView(); var view = new ExampleView();
root.Child = view;
var viewModel = new ExampleViewModel(); var viewModel = new ExampleViewModel();
Assert.Null(view.ViewModel); Assert.Null(view.ViewModel);
@ -26,6 +49,56 @@ namespace Avalonia.ReactiveUI.UnitTests
view.DataContext = null; view.DataContext = null;
Assert.Null(view.ViewModel); Assert.Null(view.ViewModel);
Assert.Null(view.DataContext); 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;
Assert.NotNull(view.ViewModel);
Assert.NotNull(view.DataContext);
Assert.True(view.ViewModel.IsActive);
root.Child = null;
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;
Assert.NotNull(view.ViewModel);
Assert.NotNull(view.DataContext);
Assert.True(view.ViewModel.IsActive);
root.Child = null;
Assert.NotNull(view.ViewModel);
Assert.NotNull(view.DataContext);
Assert.False(view.ViewModel.IsActive);
} }
} }
} }

87
tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs

@ -1,4 +1,4 @@
using Avalonia.Controls; using System.Reactive.Disposables;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using ReactiveUI; using ReactiveUI;
using Splat; using Splat;
@ -8,10 +8,30 @@ namespace Avalonia.ReactiveUI.UnitTests
{ {
public class ReactiveWindowTest public class ReactiveWindowTest
{ {
public class ExampleViewModel : ReactiveObject { } 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 class ExampleWindow : ReactiveWindow<ExampleViewModel> { }
public ReactiveWindowTest() =>
Locator
.CurrentMutable
.RegisterConstant(
new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
[Fact] [Fact]
public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model()
{ {
@ -19,16 +39,73 @@ namespace Avalonia.ReactiveUI.UnitTests
{ {
var view = new ExampleWindow(); var view = new ExampleWindow();
var viewModel = new ExampleViewModel(); var viewModel = new ExampleViewModel();
view.Show();
Assert.Null(view.ViewModel); Assert.Null(view.ViewModel);
Assert.Null(view.DataContext);
view.DataContext = viewModel; view.DataContext = viewModel;
Assert.Equal(view.ViewModel, viewModel); Assert.Equal(viewModel, view.ViewModel);
Assert.Equal(view.DataContext, viewModel); Assert.Equal(viewModel, view.DataContext);
view.DataContext = null; view.DataContext = null;
Assert.Null(view.ViewModel); Assert.Null(view.ViewModel);
Assert.Null(view.DataContext); 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();
Assert.NotNull(view.ViewModel);
Assert.NotNull(view.DataContext);
Assert.True(view.ViewModel.IsActive);
view.Close();
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();
Assert.NotNull(view.ViewModel);
Assert.NotNull(view.DataContext);
Assert.True(view.ViewModel.IsActive);
view.Close();
Assert.NotNull(view.ViewModel);
Assert.NotNull(view.DataContext);
Assert.False(view.ViewModel.IsActive);
} }
} }
} }
} }

Loading…
Cancel
Save