Browse Source

Merge branch 'master' into datagrid-integration

pull/2109/head
Jumar Macato 8 years ago
committed by GitHub
parent
commit
39a13675e3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Controls/Button.cs
  2. 76
      src/Avalonia.Controls/DropDown.cs
  3. 2
      src/Avalonia.Controls/MenuItem.cs
  4. 2
      src/Avalonia.Native/ScreenImpl.cs
  5. 224
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  6. 104
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

2
src/Avalonia.Controls/Button.cs

@ -217,7 +217,7 @@ namespace Avalonia.Controls
var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e);
if (Command != null)
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;

76
src/Avalonia.Controls/DropDown.cs

@ -56,6 +56,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
private IDisposable _subscriptionsOnOpen;
/// <summary>
/// Initializes static members of the <see cref="DropDown"/> class.
@ -149,16 +150,12 @@ namespace Avalonia.Controls
{
if (e.Key == Key.Down)
{
if (++SelectedIndex >= ItemCount)
SelectedIndex = 0;
SelectNext();
e.Handled = true;
}
else if (e.Key == Key.Up)
{
if (--SelectedIndex < 0)
SelectedIndex = ItemCount - 1;
SelectPrev();
e.Handled = true;
}
}
@ -174,6 +171,32 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if (!e.Handled)
{
if (!IsDropDownOpen)
{
if (IsFocused)
{
if (e.Delta.Y < 0)
SelectNext();
else
SelectPrev();
e.Handled = true;
}
}
else
{
e.Handled = true;
}
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@ -223,6 +246,9 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
if (CanFocus(this))
{
Focus();
@ -232,6 +258,22 @@ namespace Avalonia.Controls
private void PopupOpened(object sender, EventArgs e)
{
TryFocusSelectedItem();
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null)
{
_subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
{
ev.Handled = true;
}
}, Interactivity.RoutingStrategies.Tunnel);
}
}
private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
@ -247,7 +289,7 @@ namespace Avalonia.Controls
{
var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
if(container == null && SelectedItems.Count > 0)
if (container == null && SelectedItems.Count > 0)
{
ScrollIntoView(SelectedItems[0]);
container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
@ -307,5 +349,25 @@ namespace Avalonia.Controls
}
}
}
private void SelectNext()
{
int next = SelectedIndex + 1;
if (next >= ItemCount)
next = 0;
SelectedIndex = next;
}
private void SelectPrev()
{
int prev = SelectedIndex - 1;
if (prev < 0)
prev = ItemCount - 1;
SelectedIndex = prev;
}
}
}

2
src/Avalonia.Controls/MenuItem.cs

@ -287,7 +287,7 @@ namespace Avalonia.Controls
/// <param name="e">The click event args.</param>
protected virtual void OnClick(RoutedEventArgs e)
{
if (Command != null)
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;

2
src/Avalonia.Native/ScreenImpl.cs

@ -41,7 +41,7 @@ namespace Avalonia.Native
public void Dispose ()
{
_native.Dispose();
_native?.Dispose();
_native = null;
}
}

224
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -0,0 +1,224 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
using ReactiveUI;
using Splat;
namespace Avalonia
{
/// <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 : UserControl, IActivatable, IEnableLogger
{
/// <summary>
/// The router dependency property.
/// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary>
public RoutedViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyObservable(x => x.Router.CurrentViewModel)
.DistinctUntilChanged()
.Subscribe(NavigateToViewModel)
.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 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 animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </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>
/// <exception cref="Exception">
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
private void NavigateToViewModel(IRoutableViewModel viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null, falling back to default content.");
UpdateContent(DefaultContent);
return;
}
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(viewModel);
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?");
this.Log().Info($"Ready to show {view} with autowired {viewModel}.");
view.ViewModel = viewModel;
UpdateContent(view);
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration)
{
return new Avalonia.Animation.Animation
{
Duration = duration,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = from
}
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = to
}
},
Cue = new Cue(1d)
}
}
};
}
}
}

104
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@ -0,0 +1,104 @@
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;
namespace Avalonia
{
public class RoutedViewHostTest
{
public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "first";
public IScreen HostScreen { get; set; }
}
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "second";
public IScreen HostScreen { get; set; }
}
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class ScreenViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; } = new RoutingState();
}
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>));
}
[Fact]
public void RoutedViewHostShouldStayInSyncWithRoutingState()
{
var screen = new ScreenViewModel();
var defaultContent = new TextBlock();
var host = new RoutedViewHost
{
Router = screen.Router,
DefaultContent = defaultContent,
FadeOutAnimation = null,
FadeInAnimation = null
};
var root = new TestRoot
{
Child = host
};
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
screen.Router.Navigate
.Execute(new FirstRoutableViewModel())
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
screen.Router.Navigate
.Execute(new SecondRoutableViewModel())
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(SecondRoutableView), host.Content.GetType());
screen.Router.NavigateBack
.Execute(Unit.Default)
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
screen.Router.NavigateBack
.Execute(Unit.Default)
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
}
}
}
Loading…
Cancel
Save