我在UWP Windows 10应用程序中发现了一个奇怪的行为。即使我从页面导航回来并且页面被卸载,页面仍然会继续发出事件。例如,我导航的旧页面即使进入完全不同的页面,仍会发出“LayoutUpdated”事件。
我准备了一个最小的例子来证明这一点(代码如下)。这很简单:
共有2页:MainPage和ExamplePage。您可以从MainPage转到ExamplePage,然后可以从ExamplePage返回到MainPage。
每次导航到ExamplePage时,都会为新创建的页面提供一个新ID(页面不会被缓存)。
ExamplePage中的网格发出LayoutChanged事件。事件处理程序将文本写入调试控制台,如:“第0页上更新的网格布局”。 0是我给该页面的页面ID。
如果您来回走几次,您会看到旧页面仍然将布局更新文本写入控制台。例如,如果我转到ID为3的页面,它会写入控制台:
第0页更新了网格布局
第3页更新了网格布局
第1页更新了网格布局
第2页更新了网格布局
请注意旧页面仍会更新其布局。旧页面不应再发出任何事件,但它们不断发出事件,尽管无法再导航到它们并且它们被卸载了。
这是代码,有5个文件,只需在VS2015中创建一个新的UWP项目,然后:
MainPage.xaml中
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button x:Name="NavigationButton"
Click="NavigationButton_Click"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="0,20,0,0">Navigate</Button>
</Grid>
MainPage.xaml.cs中
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App7
{
public sealed partial class MainPage : Page
{
private App app;
public MainPage()
{
this.InitializeComponent();
app = (App)Application.Current;
}
private void NavigationButton_Click(object sender, RoutedEventArgs e)
{
var viewModel = new ExamplePageViewModel(app.GetPageId());
Frame.Navigate(typeof(ExamplePage), viewModel);
}
}
}
ExamplePage.xaml
<Grid x:Name="MainGrid"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
LayoutUpdated="MainGrid_LayoutUpdated">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button x:Name="NavigationButton"
Click="NavigationButton_Click" HorizontalAlignment="Center"
Margin="0,20,0,0">Go Back</Button>
<TextBlock Text="{Binding PageId}"
Grid.Row="1"
FontSize="30"
HorizontalAlignment="Center"></TextBlock>
</Grid>
ExamplePage.xaml.cs
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace App7
{
public sealed partial class ExamplePage : Page
{
private ExamplePageViewModel viewModel;
public ExamplePage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.NavigationMode == NavigationMode.New ||
e.NavigationMode == NavigationMode.Back)
{
viewModel = (ExamplePageViewModel)e.Parameter;
DataContext = viewModel;
}
}
private void NavigationButton_Click(object sender, RoutedEventArgs e)
{
Frame.GoBack();
}
private void MainGrid_LayoutUpdated(object sender, object e)
{
Debug.WriteLine("grid layout updated on page " + viewModel?.PageId.ToString());
}
}
}
ExamplePageViewModel.cs
using System.ComponentModel;
using Windows.UI.Xaml;
namespace App7
{
public class ExamplePageViewModel : INotifyPropertyChanged
{
private App app;
private int pageId;
public event PropertyChangedEventHandler PropertyChanged;
public int PageId
{
get
{
return pageId;
}
}
public ExamplePageViewModel(int pageId)
{
app = (App)Application.Current;
this.pageId = pageId;
}
}
}
注意:viewmodel只是为了清楚地看到哪个页面仍在发出事件。您可以删除viewmodel,它不会改变问题。
答案 0 :(得分:1)
如果垃圾收集器尚未收集该元素,则会为不在主可视树中的元素触发LayoutUpdated事件。因为我们不知道Frame类的实现,所以我们不知道它如何引用它实例化的页面(也许它对待卸载的页面的引用比它需要的时间稍长?谁知道呢。)
这与垃圾收集器的异步性质一起意味着旧页面仍然可以引发LayoutUpdated事件,直到删除事件处理程序或GC收集对象为止。在您的示例中,GC根本没有收集旧页面。
如果您的内存中仍有多个复杂页面,这是否会降低应用程序性能?我可以在我的应用程序上看到,数十个复杂页面仍在触发LayoutUpdated事件,因此所有控件都在每个页面导航上计算它们的大小,对吗?
在下一个垃圾收集周期中,GC应收集这些页面,这将在必要时自动发生。您可以使用GC.Collect()
强制进行垃圾回收,但我不建议这样做。 GC确定执行收集的时间比(通常)更好。
对所有元素(我认为)触发LayoutUpdated事件,无论该特定元素的布局是否已更改。如果您阅读该事件的docs,则说明如果元素的布局受到兄弟(例如)的影响,则必须执行此操作。
布局系统已经过优化。我不认为每次收到LayoutUpdated事件时都会对所有元素执行复杂的布局传递,所以我不担心。但是,当元素不可见时,确保在这些事件处理程序中不进行不必要的计算非常重要。
Here is another page很好地解释了LayoutUpdated事件。这是一个静态事件,如果任何元素的任何布局都更新了,它就会被触发。
我不会将任何不必要的代码添加到LayoutUpdated事件中,或者我会在导航时将它们解除绑定但是它仍将自行重新计算所有控件的大小,对吧?
您对LayoutUpdated事件的回应应该是“某个地方的某个元素的布局已更新,这可能会影响我,也可能不会影响我”。您可以取消绑定该事件,或者您只需检查this.Parent
或this.Frame
是否为空并挽救,在这种情况下,Page不在框架中。
即使控件不在可视树中,是否还有其他事件会触发?如何找到此类活动的清单?
我不确定。您需要针对这些情况测试您的应用,只需在每个事件处理程序中放置一个断点,以便您知道它是否会被触发。