每一个C# UWP应用中,必不可少的一个文件就是App.xaml.cs。App类是整个程序的入口,它继承了Application类。在App.xaml.cs中,我们可以通过重写Application类中的一些函数,来自定义程序启动时的行为。

在默认UWP模板中,OnLaunched函数担任了初始化程序窗口的工作。但实际上,一个UWP应用的生命周期中,OnLaunched函数可能被调用任意次(包括0次或者很多次)。所以,在初始化程序窗口的时候,必须考虑重复初始化的问题。新手很容易犯的一个错误是在OnLaunched函数中绑定OnBackRequested函数,这样会导致在某些情况下,按一次后退键后退多个页面,或是在某些情况下后退键无效的问题。

注意:本文中代码在Visual Studio 2015下测试过,面向的操作系统目标版本为10.0.14393,最低版本为10.0.10586。完整的示例代码在文末。

目录:

激活函数
处理非正常退出
预启动应用
帧频计数器
一个处理后退键的例子
总结

激活函数

App类中,通过不同方式启动或激活应用时会调用OnLaunched,OnFileActivated,OnFileSavePickerActivated等函数。在此我们将它们统称为“激活函数”。激活函数中必不可少的一项工作是初始化应用界面,如果不这么做的话,应用启动后会一直停留在初始界面没有反应。以下函数是最简单的初始化界面的函数:

private void CreateRootFrameAndNavigate(Type page, object parameter, bool forceNavigate)
{
    Frame rootFrame = Window.Current.Content as Frame;

    // 若不存在rootFrame,则创建一个rootFrame
    if (rootFrame == null)
    {
        rootFrame = new Frame();
        // OnNavigationFailed函数在默认UWP模板中的App.xaml.cs中有定义
        rootFrame.NavigationFailed += OnNavigationFailed;
        Window.Current.Content = rootFrame;
    }

    // 若rootFrame是新创建的,或是强制要求导航到页面,则导航到指定的页面
    if (rootFrame.Content == null || forceNavigate)
    {
        rootFrame.Navigate(page, parameter);
    }

    Window.Current.Activate();
}

然后,你可以将默认的OnLaunched函数替换成以下内容, 它的功能由与默认OnLaunched函数是几乎相同的:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    // 若应用已经启动,则不强制导航到MainPage
    CreateRootFrameAndNavigate(typeof(MainPage), null, false);
}

然后,如果你的应用支持打开文件的话,你的OnFileActivated函数应该是这样的:

protected override void OnFileActivated(FileActivatedEventArgs e)
{
    // 即使应用已经启动,也要跳转到查看文件的页面
    CreateRootFrameAndNavigate(typeof(ViewFilePage), e.Files, true);
}

注意:由于在应用程序生命周期中,应用有可能会被通过任一方式激活,所以每一个函数都可能会被调用一次或多次。例如OnLaunched函数在每次用户点击开始菜单的应用图标时都会执行。所以在这些激活函数中必须做一些判断,避免重复初始化。(上方的CreateRootFrameAndNavigate函数通过判断rootFrame是否存在的方式避免的重复初始化。)

处理非正常退出

上面说到的这些激活函数都只有一个参数,类型为xxxEventArgs,它们都实现接口IActivatedEventArgs接口,拥有PreviousExecutionState这个表明之前应用状态的成员。若应用上一次运行时被非正常结束(例如关机、断电、被任务管理器结束、因为内存不足被关闭等),PreviousExecutionState的值会是Terminated,按照微软默认UWP模板的做法,这时应该尝试为用户恢复上次退出时的状态。注意:因为应用被非正常结束后,下次启动的方式是不确定的,所以你应该在你支持的任何一个激活函数中判断PreviousExecutionState,并尝试恢复非正常退出的状态。

所以,你现在要将你的激活函数改成这样:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
    {
        // TODO: 恢复非正常退出前的状态
    }

    // 若应用已经启动,则不强制导航到MainPage
    CreateRootFrameAndNavigate(typeof(MainPage), null, false);
}

protected override void OnFileActivated(FileActivatedEventArgs e)
{
    if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
    {
        // TODO: 恢复非正常退出前的状态
    }

    // 即使应用已经启动,也要跳转到查看文件的页面
    CreateRootFrameAndNavigate(typeof(ViewFilePage), e.Files, true);
}

预启动应用

OnLaunched函数在Windows系统预启动应用时会执行。

预启动(Prelaunch)指的是Windows系统为了提高应用启动速度,在系统资源充足时主动预先启动一些应用。用户不会观察到预启动行为,预启动的应用在预启动后会马上被挂起(Suspend)。所以,预启动时,应当只加载程序,而不应该加载新闻、消息等容易变化的信息,否则用户真正启动应用时会看到过时的信息。若应用是被预启动的,args.PrelaunchActivated的值为true,否则为false。在Windows 10 1607(10.0.14393)以上版本中,只有你调用CoreApplication.EnablePrelaunch(true)表明应用支持预启动后,你的应用才会被预启动;在Windows 10 1511(10.0.10586)以下版本中,若你的应用不支持预启动,你应当在args.PrelaunchActivated为true时不执行任何操作,直接return。(参见处理应用预启动。)

为了正确处理预启动,或者是正确声明应用不支持预启动,OnLaunched函数应该改为:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    if (e.PrelaunchActivated)
    {
        // TODO: 若要支持预启动,在此处处理预启动,否则保留原状。
        return;
    }

    if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
    {
        // TODO: 恢复非正常退出前的状态
    }

    // 若应用已经启动,则不强制导航到MainPage
    CreateRootFrameAndNavigate(typeof(MainPage), null, false);
}

帧频计数器

默认UWP模板中,当通过调试器启动应用时,会显示帧频计数器。若要实现此功能,在你的激活函数中调用显示帧频计数器的代码:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    // 调试状态下显示帧计数器
#if DEBUG
    if (System.Diagnostics.Debugger.IsAttached)
    {
        this.DebugSettings.EnableFrameRateCounter = true;
    }
#endif

    if (e.PrelaunchActivated)
    {
        // TODO: 若要支持预启动,在此处处理预启动,否则保留原状。
        return;
    }

    if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
    {
        // TODO: 恢复非正常退出前的状态
    }

    // 若应用已经启动,则不强制导航到MainPage
    CreateRootFrameAndNavigate(typeof(MainPage), null, false);
}

一个处理后退键的例子

下面这个例子能够实现当可以后退时,在桌面系统中左上角显示后退键,并且通过手机后退键、平板模式任务栏后退键、桌面窗口左上角后退键、Windows + Backspace快捷键都能够正常实现后退功能。

在每一个创建RootFrame的位置,绑定Navigated函数:在

rootFrame = new Frame();

后面加上

rootFrame.Navigated += OnNavigated;

然后定义OnNavigated函数,根据当前是否可以后退的状态来改变后退键的显示状态:

private void OnNavigated(object sender, NavigationEventArgs e)
{
    Frame frame = sender as Frame;
    SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
        frame.CanGoBack ?
            AppViewBackButtonVisibility.Visible :
            AppViewBackButtonVisibility.Collapsed;
}

由于OnWindowCreated函数对于每一个应用窗口只会执行一次,而后退键的操作也是应用于每一个窗口的,所以OnWindowCreated函数是绑定后退键事件的绝佳位置:

protected override void OnWindowCreated(WindowCreatedEventArgs args)
{
    SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;
}

private void OnBackRequested(object sender, BackRequestedEventArgs e)
{
    Frame rootFrame = Window.Current.Content as Frame;
    if (rootFrame.CanGoBack)
    {
        rootFrame.GoBack();
        e.Handled = true;
    }
    else
    {
        e.Handled = false;
    }
}

这样,即使你有多个窗口,每个窗口也能很好地独立处理窗口内的后退键行为。

总结

综上所述,一个实现最简单功能,能够正确处理普通启动和文件启动,能够正确处理后退键的App类代码如下(如果你的应用不支持打开文件的话,将OnFileActivated函数删除即可)。它完成了一个基本的UWP应用在App类中该做的事。

public sealed partial class App : Application
{
    public App()
    {
        this.InitializeComponent();
    }

    protected override void OnLaunched(LaunchActivatedEventArgs e)
    {
        // 调试状态下显示帧计数器
#if DEBUG
        if (System.Diagnostics.Debugger.IsAttached)
        {
            this.DebugSettings.EnableFrameRateCounter = true;
        }
#endif

        if (e.PrelaunchActivated)
        {
            // TODO: 若要支持预启动,在此处处理预启动,否则保留原状。
            return;
        }

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            // TODO: 恢复非正常退出前的状态
        }

        // 若应用已经启动,则不强制导航到MainPage
        CreateRootFrameAndNavigate(typeof(MainPage), null, false);
    }

    protected override void OnFileActivated(FileActivatedEventArgs e)
    {
        // 调试状态下显示帧计数器
#if DEBUG
        if (System.Diagnostics.Debugger.IsAttached)
        {
            this.DebugSettings.EnableFrameRateCounter = true;
        }
#endif

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            // TODO: 恢复非正常退出前的状态
        }

        // 即使应用已经启动,也要跳转到查看文件的页面
        CreateRootFrameAndNavigate(typeof(ViewFilePage), e.Files, true);
    }

    protected override void OnWindowCreated(WindowCreatedEventArgs args)
    {
        // 关联按下返回键的事件。
        SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;
    }

    private void CreateRootFrameAndNavigate(Type page, object parameter, bool forceNavigate)
    {
        Frame rootFrame = Window.Current.Content as Frame;

        // 若不存在rootFrame,则创建一个rootFrame
        if (rootFrame == null)
        {
            rootFrame = new Frame();
            rootFrame.Navigated += OnNavigated;
            rootFrame.NavigationFailed += OnNavigationFailed;
            Window.Current.Content = rootFrame;
        }

        // 若rootFrame是新创建的,或是强制要求导航到页面,则导航到指定的页面
        if (rootFrame.Content == null || forceNavigate)
        {
            rootFrame.Navigate(page, parameter);
        }

        Window.Current.Activate();
    }

    private void OnNavigated(object sender, NavigationEventArgs e)
    {
        Frame frame = sender as Frame;

        // 若可以后退,则显示后退键,否则隐藏后退键。
        SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
            frame.CanGoBack ?
                AppViewBackButtonVisibility.Visible :
                AppViewBackButtonVisibility.Collapsed;
    }

    private void OnBackRequested(object sender, BackRequestedEventArgs e)
    {
        // 若可以后退,则后退。若不能后退,则交给其他的程序处理。
        Frame rootFrame = Window.Current.Content as Frame;
        if (rootFrame.CanGoBack)
        {
            rootFrame.GoBack();
            e.Handled = true;
        }
        else
        {
            e.Handled = false;
        }
    }

    private void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
    {
        throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
    }
}