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.VisualTree;
using Avalonia.Controls;
@ -6,8 +9,10 @@ using ReactiveUI;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// A ReactiveUI UserControl that implements <see cref="IViewFor{TViewModel}"/>
/// and will activate your ViewModel automatically if it supports activation.
/// 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
@ -20,7 +25,14 @@ namespace Avalonia.ReactiveUI
/// </summary>
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>

18
src/Avalonia.ReactiveUI/ReactiveWindow.cs

@ -1,3 +1,6 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.VisualTree;
using Avalonia.Controls;
@ -6,8 +9,10 @@ using ReactiveUI;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// A ReactiveUI Window that implements <see cref="IViewFor{TViewModel}"/>
/// and will activate your ViewModel automatically if it supports activation.
/// 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
@ -20,7 +25,14 @@ namespace Avalonia.ReactiveUI
/// </summary>
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>

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

@ -74,44 +74,26 @@ namespace Avalonia.ReactiveUI.UnitTests
{
public ActivatableWindow()
{
InitializeComponent();
Assert.IsType<Border>(Content);
Content = new Border();
this.WhenActivated(disposables => { });
}
private void InitializeComponent()
{
AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'>
<Border/>
</Window>", null, this);
}
}
public class ActivatableUserControl : ReactiveUserControl<ActivatableViewModel>
{
public ActivatableUserControl()
{
InitializeComponent();
Assert.IsType<Border>(Content);
Content = new Border();
this.WhenActivated(disposables => { });
}
private void InitializeComponent()
{
AvaloniaRuntimeXamlLoader.Load(@"
<UserControl xmlns='https://github.com/avaloniaui'>
<Border/>
</UserControl>", null, this);
}
}
public AvaloniaActivationForViewFetcherTest()
{
Locator.CurrentMutable.RegisterConstant(
new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
}
public AvaloniaActivationForViewFetcherTest() =>
Locator
.CurrentMutable
.RegisterConstant(
new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
[Fact]
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 ReactiveUI;
using Splat;
@ -8,14 +8,37 @@ namespace Avalonia.ReactiveUI.UnitTests
{
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 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);
@ -26,6 +49,56 @@ namespace Avalonia.ReactiveUI.UnitTests
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;
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 ReactiveUI;
using Splat;
@ -8,10 +8,30 @@ namespace Avalonia.ReactiveUI.UnitTests
{
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 ReactiveWindowTest() =>
Locator
.CurrentMutable
.RegisterConstant(
new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
[Fact]
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 viewModel = new ExampleViewModel();
view.Show();
Assert.Null(view.ViewModel);
Assert.Null(view.DataContext);
view.DataContext = viewModel;
Assert.Equal(view.ViewModel, viewModel);
Assert.Equal(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();
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