Issue #62: Fixed NavigateToPageAction to find INavigate ancestors

The problem was NavigateToPageAction's management of sender vs.
senderObject in Execute's loop looking for an INavigate implementation.
Both implementations (managed and native) had similar problems where the
current element's visual parent was found, but wasn't actually analyzed on
the next time around the loop.

The fixes in the Execute methods are tiny.  The rest of the changes are
all about unit testing the managed fix.  I didn't invest in unit tests for
the native change, but did verify it manually in a test app.
This commit is contained in:
Erik De Bonte 2015-12-17 17:05:29 -08:00 коммит произвёл debonte
Родитель 0645fc5dae
Коммит 6e66f8f50d
11 изменённых файлов: 242 добавлений и 7 удалений

Просмотреть файл

@ -0,0 +1,13 @@
<Page
x:Class="ManagedUnitTests.BlankPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ManagedUnitTests"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
</Grid>
</Page>

Просмотреть файл

@ -0,0 +1,21 @@
using Windows.UI.Xaml.Controls;
namespace ManagedUnitTests
{
/// <summary>
/// This page serves two purposes:
///
/// 1. Its existence causes the XAML compiler to implement IXamlMetadataProvider
/// on the App class. IXamlMetadataProvider is used by NavigateToPageAction and will
/// only be implemented on App if there are XAML types defined in the project.
///
/// 2. It provides a target for the NavigateToPageAction to navigate to in tests.
/// </summary>
public sealed partial class BlankPage : Page
{
public BlankPage()
{
this.InitializeComponent();
}
}
}

Просмотреть файл

@ -90,6 +90,7 @@
</PropertyGroup>
<ItemGroup>
<!--A reference to the entire .Net Framework and Windows SDK are automatically included-->
<None Include="..\..\..\key\35MSSharedLib1024.snk" />
<None Include="project.json" />
<SDKReference Include="MSTestFramework.Universal, Version=$(UnitTestPlatformVersion)" />
<SDKReference Include="TestPlatform.Universal, Version=$(UnitTestPlatformVersion)" />
@ -97,10 +98,15 @@
<ItemGroup>
<Compile Include="ActionCollectionTest.cs" />
<Compile Include="BehaviorCollectionTest.cs" />
<Compile Include="BlankPage.xaml.cs">
<DependentUpon>BlankPage.xaml</DependentUpon>
</Compile>
<Compile Include="InteractionTest.cs" />
<Compile Include="NavigateToPageActionTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Stubs.cs" />
<Compile Include="TestUitilties.cs" />
<Compile Include="TestVisualTreeHelper.cs" />
<Compile Include="UnitTestApp.xaml.cs">
<DependentUpon>UnitTestApp.xaml</DependentUpon>
</Compile>
@ -140,9 +146,20 @@
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup>
<Page Include="BlankPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<DelaySign>true</DelaySign>
<AssemblyOriginatorKeyFile>..\..\..\key\35MSSharedLib1024.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

Просмотреть файл

@ -0,0 +1,71 @@
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Microsoft.Xaml.Interactions.Core;
using Windows.UI.Xaml;
using AppContainerUITestMethod = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethodAttribute;
namespace ManagedUnitTests
{
[TestClass]
public class NavigateToPageActionTest
{
private static readonly string TestPageName = typeof(BlankPage).FullName;
[AppContainerUITestMethod]
public void Execute_SenderImplementsINavigate_NavigatesToSender()
{
// Arrange
TestVisualTreeHelper visualTreeHelper = new TestVisualTreeHelper();
NavigateToPageAction action = new NavigateToPageAction(visualTreeHelper);
action.TargetPage = NavigateToPageActionTest.TestPageName;
NavigableStub navigateTarget = new NavigableStub();
// Act
bool success = (bool)action.Execute(navigateTarget, null);
// Assert
Assert.IsTrue(success);
Assert.AreEqual(NavigateToPageActionTest.TestPageName, navigateTarget.NavigatedTypeFullName);
}
[AppContainerUITestMethod]
public void Execute_SenderDoesNotImplementINavigate_NavigatesToAncestor()
{
// Arrange
TestVisualTreeHelper visualTreeHelper = new TestVisualTreeHelper();
NavigateToPageAction action = new NavigateToPageAction(visualTreeHelper);
action.TargetPage = NavigateToPageActionTest.TestPageName;
DependencyObject sender = new SimpleDependencyObject();
NavigableStub navigateTarget = new NavigableStub();
visualTreeHelper.AddChild(navigateTarget, sender);
// Act
bool success = (bool)action.Execute(sender, null);
// Assert
Assert.IsTrue(success);
Assert.AreEqual(NavigateToPageActionTest.TestPageName, navigateTarget.NavigatedTypeFullName);
}
[AppContainerUITestMethod]
public void Execute_NoAncestorImplementsINavigate_Fails()
{
// Arrange
TestVisualTreeHelper visualTreeHelper = new TestVisualTreeHelper();
NavigateToPageAction action = new NavigateToPageAction(visualTreeHelper);
action.TargetPage = NavigateToPageActionTest.TestPageName;
DependencyObject sender = new SimpleDependencyObject();
DependencyObject parent = new SimpleDependencyObject();
visualTreeHelper.AddChild(parent, sender);
// Act
bool success = (bool)action.Execute(sender, null);
// Assert
Assert.IsFalse(success);
}
private class SimpleDependencyObject : DependencyObject
{
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Microsoft.Xaml.Interactions.Utility;
using Windows.UI.Xaml;
namespace ManagedUnitTests
{
/// <summary>
/// Test impementation of VisualTreeHelper. Enables unit testing of code that
/// uses VisualTreeHelper without needing a real visual tree.
/// </summary>
internal class TestVisualTreeHelper : IVisualTreeHelper
{
private Dictionary<DependencyObject, DependencyObject> parents = new Dictionary<DependencyObject, DependencyObject>();
public void AddChild(DependencyObject parent, DependencyObject child)
{
this.parents[child] = parent;
}
#region IVisualTreeHelper implementation
public DependencyObject GetParent(DependencyObject child)
{
DependencyObject parent;
this.parents.TryGetValue(child, out parent);
return parent;
}
#endregion
}
}

Просмотреть файл

@ -2,17 +2,19 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.Xaml.Interactions.Core
{
using Microsoft.Xaml.Interactions.Utility;
using Microsoft.Xaml.Interactivity;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
using Interactivity;
/// <summary>
/// An action that switches the current visual to the specified <see cref="Windows.UI.Xaml.Controls.Page"/>.
/// </summary>
public sealed class NavigateToPageAction : DependencyObject, IAction
{
private readonly IVisualTreeHelper visualTreeHelper;
/// <summary>
/// Identifies the <seealso cref="TargetPage"/> dependency property.
/// </summary>
@ -33,6 +35,26 @@ namespace Microsoft.Xaml.Interactions.Core
typeof(NavigateToPageAction),
new PropertyMetadata(null));
/// <summary>
/// Initializes a new instance of the NavigateToPageAction class.
/// </summary>
public NavigateToPageAction()
: this(new UwpVisualTreeHelper())
{
}
/// <summary>
/// Initializes a new instance of the NavigateToPageAction class.
/// </summary>
/// <param name="visualTreeHelper">
/// IVisualTreeHelper implementation to use when searching the tree for an
/// INavigate target.
/// </param>
internal NavigateToPageAction(IVisualTreeHelper visualTreeHelper)
{
this.visualTreeHelper = visualTreeHelper;
}
/// <summary>
/// Gets or sets the fully qualified name of the <see cref="Windows.UI.Xaml.Controls.Page"/> to navigate to. This is a dependency property.
/// </summary>
@ -76,7 +98,15 @@ namespace Microsoft.Xaml.Interactions.Core
return false;
}
IXamlMetadataProvider metadataProvider = (IXamlMetadataProvider)Application.Current;
IXamlMetadataProvider metadataProvider = Application.Current as IXamlMetadataProvider;
if (metadataProvider == null)
{
// This will happen if there are no XAML files in the project other than App.xaml.
// The markup compiler doesn't bother implementing IXamlMetadataProvider on the app
// in that case.
return false;
}
IXamlType xamlType = metadataProvider.GetXamlType(this.TargetPage);
if (xamlType == null)
{
@ -90,10 +120,10 @@ namespace Microsoft.Xaml.Interactions.Core
// root we were given for another INavigate.
while (senderObject != null && navigateElement == null)
{
navigateElement = sender as INavigate;
navigateElement = senderObject as INavigate;
if (navigateElement == null)
{
senderObject = VisualTreeHelper.GetParent(senderObject);
senderObject = this.visualTreeHelper.GetParent(senderObject);
}
}

Просмотреть файл

@ -145,6 +145,8 @@
<Compile Include="Media\PlaySoundAction.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\Version\Version.cs" />
<Compile Include="Utility\IVisualTreeHelper.cs" />
<Compile Include="Utility\UwpVisualTreeHelper.cs" />
<EmbeddedResource Include="Properties\Microsoft.Xaml.Interactions.rd.xml" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -17,4 +17,5 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("ManagedUnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]

Просмотреть файл

@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.Xaml.Interactions.Utility
{
using Windows.UI.Xaml;
/// <summary>
/// Abstraction layer over the UWP's VisualTreeHelper class so we can
/// mock it for unit testing purposes where an actual view won't be available.
/// </summary>
internal interface IVisualTreeHelper
{
/// <summary>
/// Returns an object's parent object in the visual tree.
/// </summary>
/// <param name="reference">
/// The object for which to get the parent object.
/// </param>
/// <returns>
/// The parent object of the reference object in the visual tree.
/// </returns>
/// <remarks>
/// THREAD SAFETY: This method should be called on the object's Dispatcher thread.
/// </remarks>
DependencyObject GetParent(DependencyObject reference);
}
}

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.Xaml.Interactions.Utility
{
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
/// <summary>
/// IVisualTreeHelper implementation that calls the real VisualTreeHelper.
/// </summary>
internal class UwpVisualTreeHelper : IVisualTreeHelper
{
#region IVisualTreeHelper implementation
public DependencyObject GetParent(DependencyObject reference)
{
return VisualTreeHelper.GetParent(reference);
}
#endregion
}
}

Просмотреть файл

@ -60,7 +60,7 @@ Object^ NavigateToPageAction::Execute(Object^ sender, Object^ parameter)
navigateElement = dynamic_cast<INavigate^>(senderObject);
if (navigateElement == nullptr)
{
sender = VisualTreeHelper::GetParent(senderObject);
senderObject = VisualTreeHelper::GetParent(senderObject);
}
}