committed by
GitHub
32 changed files with 892 additions and 259 deletions
@ -0,0 +1,55 @@ |
|||
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; |
|||
|
|||
itemsControl.ItemTemplate = DefaultItemTemplate; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.ReactiveUI |
|||
{ |
|||
/// <summary>
|
|||
/// A ContentControl that animates the transition when its content is changed.
|
|||
/// </summary>
|
|||
public class TransitioningContentControl : ContentControl, IStyleable |
|||
{ |
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<IPageTransition> PageTransitionProperty = |
|||
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition), |
|||
new CrossFade(TimeSpan.FromSeconds(0.5))); |
|||
|
|||
/// <summary>
|
|||
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<object> DefaultContentProperty = |
|||
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent)); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation played when content appears and disappears.
|
|||
/// </summary>
|
|||
public IPageTransition PageTransition |
|||
{ |
|||
get => GetValue(PageTransitionProperty); |
|||
set => SetValue(PageTransitionProperty, 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 content with animation.
|
|||
/// </summary>
|
|||
public new object Content |
|||
{ |
|||
get => base.Content; |
|||
set => UpdateContentWithTransition(value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// TransitioningContentControl uses the default ContentControl
|
|||
/// template from Avalonia default theme.
|
|||
/// </summary>
|
|||
Type IStyleable.StyleKey => typeof(ContentControl); |
|||
|
|||
/// <summary>
|
|||
/// Updates the content with transitions.
|
|||
/// </summary>
|
|||
/// <param name="content">New content to set.</param>
|
|||
private async void UpdateContentWithTransition(object content) |
|||
{ |
|||
if (PageTransition != null) |
|||
await PageTransition.Start(this, null, true); |
|||
base.Content = content; |
|||
if (PageTransition != null) |
|||
await PageTransition.Start(null, this, true); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
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>
|
|||
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
|
|||
/// </summary>
|
|||
public ViewModelViewHost() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.WhenAnyValue(x => x.ViewModel) |
|||
.Subscribe(NavigateToViewModel) |
|||
.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 locator.
|
|||
/// </summary>
|
|||
public IViewLocator ViewLocator { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Invoked when ReactiveUI router navigates to a view model.
|
|||
/// </summary>
|
|||
/// <param name="viewModel">ViewModel to which the user navigates.</param>
|
|||
private void NavigateToViewModel(object viewModel) |
|||
{ |
|||
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); |
|||
if (viewInstance == null) |
|||
{ |
|||
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); |
|||
Content = DefaultContent; |
|||
return; |
|||
} |
|||
|
|||
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); |
|||
viewInstance.ViewModel = viewModel; |
|||
if (viewInstance is IStyledElement styled) |
|||
styled.DataContext = viewModel; |
|||
Content = viewInstance; |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit 1e3ffc315401f0b2eb96a0e79b25c2fc19a80d78 |
|||
Subproject commit a73c5234831267b23160e01a9fbc83be633f69fc |
|||
@ -0,0 +1,9 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using 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)] |
|||
@ -0,0 +1,116 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using 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(); |
|||
|
|||
public ExampleView() |
|||
{ |
|||
Content = List; |
|||
ViewModel = new ExampleViewModel(); |
|||
this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items); |
|||
} |
|||
} |
|||
|
|||
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); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Use_View_Model_View_Host_As_Data_Template() |
|||
{ |
|||
var view = new ExampleView(); |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
view.List.Template = GetTemplate(); |
|||
view.List.ApplyTemplate(); |
|||
view.List.Presenter.ApplyTemplate(); |
|||
|
|||
var child = view.List.Presenter.Panel.Children[0]; |
|||
var container = (ContentPresenter) child; |
|||
container.UpdateChild(); |
|||
|
|||
Assert.IsType<ViewModelViewHost>(container.Child); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Resolve_And_Embedd_Appropriate_View_Model() |
|||
{ |
|||
var view = new ExampleView(); |
|||
var root = new TestRoot { Child = view }; |
|||
view.ViewModel.Items.Add(new NestedViewModel()); |
|||
|
|||
view.List.Template = GetTemplate(); |
|||
view.List.ApplyTemplate(); |
|||
view.List.Presenter.ApplyTemplate(); |
|||
|
|||
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); |
|||
} |
|||
|
|||
private FuncControlTemplate GetTemplate() |
|||
{ |
|||
return new FuncControlTemplate<ItemsControl>(parent => |
|||
{ |
|||
return new Border |
|||
{ |
|||
Background = new Media.SolidColorBrush(0xffffffff), |
|||
Child = new ItemsPresenter |
|||
{ |
|||
Name = "PART_ItemsPresenter", |
|||
MemberSelector = parent.MemberSelector, |
|||
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], |
|||
} |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ReactiveUserControlTest |
|||
{ |
|||
public class ExampleViewModel : ReactiveObject { } |
|||
|
|||
public class ExampleView : ReactiveUserControl<ExampleViewModel> { } |
|||
|
|||
[Fact] |
|||
public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() |
|||
{ |
|||
var view = new ExampleView(); |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.UnitTests; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.ReactiveUI.UnitTests |
|||
{ |
|||
public class ReactiveWindowTest |
|||
{ |
|||
public class ExampleViewModel : ReactiveObject { } |
|||
|
|||
public class ExampleWindow : ReactiveWindow<ExampleViewModel> { } |
|||
|
|||
[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(); |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.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_Should_Derive_Template_From_Content_Control() |
|||
{ |
|||
var target = new TransitioningContentControl(); |
|||
var stylable = (IStyledElement)target; |
|||
Assert.Equal(typeof(ContentControl),stylable.StyleKey); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Transitioning_Control_Template_Should_Be_Instantiated() |
|||
{ |
|||
var target = new TransitioningContentControl |
|||
{ |
|||
PageTransition = null, |
|||
Template = GetTemplate(), |
|||
Content = "Foo" |
|||
}; |
|||
target.ApplyTemplate(); |
|||
((ContentPresenter)target.Presenter).UpdateChild(); |
|||
|
|||
var child = ((IVisual)target).VisualChildren.Single(); |
|||
Assert.IsType<Border>(child); |
|||
child = child.VisualChildren.Single(); |
|||
Assert.IsType<ContentPresenter>(child); |
|||
child = child.VisualChildren.Single(); |
|||
Assert.IsType<TextBlock>(child); |
|||
} |
|||
|
|||
private FuncControlTemplate GetTemplate() |
|||
{ |
|||
return new FuncControlTemplate<ContentControl>(parent => |
|||
{ |
|||
return new Border |
|||
{ |
|||
Background = new Media.SolidColorBrush(0xffffffff), |
|||
Child = new ContentPresenter |
|||
{ |
|||
Name = "PART_ContentPresenter", |
|||
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], |
|||
[~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], |
|||
} |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
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 SecondViewModel : ReactiveObject { } |
|||
|
|||
public class SecondView : ReactiveUserControl<SecondViewModel> { } |
|||
|
|||
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>)); |
|||
} |
|||
|
|||
[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 |
|||
}; |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue