Universal App - 處理 URI activation

Universal App - 處理 URI activation

在<>介紹了如何操作 Launcer 執行指定的 URI 或是 File Type 來開啟預設的 App。

該篇則對如何讓 App 註冊處理指定的 URI 類型,參考<How to handle URI activation (XAML)>來往下說明:

 

App 可以註冊處理特定的 「Uniform Resource Identifier (URI) scheme name」,不管是 Store App 或 Phone App,

[注意]

a.  Desktop apps 與 Windows Runtime apps 可註冊處理相同的 URI scheme name;

     =>當用戶選擇了 Windows Runtime apps 為預設執行 app 時,之後每次遇到該 URI scheme name 時則會啟動該 app。

 

b. 建議一個 app 註冊一個 URI scheme name,不建議處理所有的 URI scheme name (系統 URI 仍是使用系統 App);

    => Windows Phone 8.1 某些 URIs 與 File extensions 是被系統註冊的,所以加在自己的 App 裡也會被系統忽略;

 

c. 註冊了 URI scheme name 後,App 需要實作觸發後的功能以符合用戶的期待;

    =>例如:mailto,用戶會預期開啟了 App 會進入撰寫郵件的畫面…等;

    =>詳細可參考<Guidelines and checklist for file types and URIs>的說明;

 

 

接下來說明如何透過 package.appxmanifest 註冊要處理的 URI 與 App.cs 要註冊那些對應的事件來回應。

 

A. 利用 package.appxmanifest 檔案,支援 Protocol

開啟 package.appxmanifest,切換至 Declarations 頁籤,加入一個「Protocol」的宣告,依照下說明進行設定:

Type Field Description
Property Logo 設定當系統收到 URI scheme 後跳出讓用戶選擇的對話框 (Control Panel) 裡 App 顯示的圖示;
如果未設定,則使用 App 中 small logo 為識別圖
詳細可參考<Default Programs>。
  Display Name 設定當系統收到 URI scheme 後跳出讓用戶選擇的對話框 (Control Panel) 裡 App 顯示的名稱;
如果未設定,則以 App 的 Display Name 來顯示;
  Name 定義要註冊的 URI scheme。該值需要是全小寫字(all lower case letters)。

以下分別列出 App 不可以註冊的 URI scheme:
》Windows Store Apps

application.manifest, application.reference, batfile, blob, cerfile, chm.file,cmdfile, comfile, cplfile, dllfile, drvfile, exefile,explorer.assocactionid.burnselection, explorer.assocactionid.closesession,explorer.assocactionid.erasedisc, explorer.assocactionid.zipselection,explorer.assocprotocol.search-ms, explorer.burnselection,explorer.closesession, explorer.erasedisc, explorer.zipselection, file, fonfile,hlpfile, htafile, inffile , insfile, internetshortcut, jsefile, lnkfile,microsoft.powershellscript.1, ms-accountpictureprovider, ms-appdata, ms-appx, ms-autoplay, msi.package, msi.patch, ms-windows-store, ocxfile,piffile, regfile, scrfile, scriptletfile, shbfile, shcmdfile, shsfile,smb, sysfile,ttffile,unknown, usertileprovider,vbefile,vbsfile, windows.gadget,wsffile,wsfile,wshfile


》Windows Phone Store Apps

系統內鍵 App 所註冊的 URI scheme
bing, callto, dtmf, http, https, mailto, maps, ms-excel, ms-powerpoint, ms-settings-airplanemode, ms-settings-bluetooth, ms-settings-cellular, ms-settings-emailandaccounts, ms-settings-location, ms-settings-lock, ms-settings-wifi, ms-word, office, onenote, tel, wallet, xbls, zune


系統專用的 URI scheme
Explorer.AssocActionId.BurnSelection, Explorer.AssocActionId.CloseSession, Explorer.AssocActionId.EraseDisc, Explorer.AssocActionId.ZipSelection, Explorer.AssocProtocol.search-ms, Explorer.BurnSelection, Explorer.CloseSession, Explorer.EraseDisc, Explorer.ZipSelection, File, Iehistory, Ierss, Javascript, Jscript, LDAP, Res, rlogin, StickyNotes, telnet, tn3270, Vbscript, windowsmediacenterapp, windowsmediacenterssl, windowsmediacenterweb, WMP11.AssocProtocol.MMS
App settings Desired View
(Windows Only)
設定當 URI scheme name 被執行時,該 App 預計開啟多大的畫面空間。
值有五種:
Default、UseLess、UseHalf、UseMore 與 UseMinium;

[注意]
雖然指定的了 Desired View 的類型,但不能保證目前應用程式的特定畫面空間會被開啟。
因為系統會考量目前來源目標程式的優先權、屏幕取向、…等。
  Executable <Application>,

The default launch executable for the app. This file must be present in the package.
If you specify this attribute you must specify the EntryPoint attribute. If you specify this attribute you must not specify the StartPage attribute. (Not required)

  Entry point <Application>,

The activatable class ID, such as ""Office.Winword.Class".

If you specify this attribute, you must also specify theExecutable attribute. If you specify this attribute you must not specify theStartPage attribute. (Not required)

  Start page <Application>,

The default launch HTML page for the app. This file must be present in the package.

If you specify this attribute, you cannot specify either theEntryPoint attribute or theExecutable attribute.

(Not required)

 

如下圖示範例:

image

此時打開文字工具開啟:package.appxmanifest,將可以在「<Application />」項目下找到建立的 Extension element:

<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" 
             EntryPoint="AssociationUnApp.Windows.App">
  <m2:VisualElements />
  <Extensions>
    <Extension Category="windows.protocol">
      <Protocol Name="bus" m2:DesiredView="default" />
      <Logo>images\logo.png</Logo>                                        
      <DisplayName>Bus URI Scheme</DisplayName>    
    </Extension>
  </Extensions>
</Application>
</Applications>

 

 

B. 建立專用的 icons (proper icons)

由於 App 被用戶選擇為預設處理特定 URI scheme 時,該 App 將會被顯示於各種地方,例如:Default programs control panel。

<How to handle URI activation (XAML) >建議準備適用的圖示與大小來對應不同環境的呈現,如下:

Type Icon size
Windows Store app 製作 16/32/48/256 pixel versions 這些 size 的 small logo 與 icon
Windows Phone app 製作 63/129/236 pixel versions 這些 size 的 small logo 與 icon

需要注意命名:

〉for small logo:{file name}.targetsize-{size pixel}.png;

〉for icon:{file name}.tartgetsize-{size pixel}.png;

〉file name 的定義可參考<Association launching sample>;

 

參考範例如下圖:

The Solution Explorer with a view of the files in the images folder. There are 16, 32, 48, and 256 pixel versions of both ‘Icon.targetsize’ and ‘smallTile-sdk’

 

 

C. 註冊處理 App.cs 中的 OnActivated 事件

OnActivated event handler 接收所有 activation events。利用 Kind 屬性來識別觸發 OnActivated 的來源是否為 Protocol activation event

如下程式範例:

public partial class App
{
   protected override void OnActivated(IActivatedEventArgs args)
   {
      if (args.Kind == ActivationKind.Protocol)
      {
         ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs;

		
		
         // TODO: Handle URI activation
         // The received URI is eventArgs.Uri.AbsoluteUri
      }
   }
}

在該事件裡可以處理預計的事件,例如:開啟特定的畫面或邏輯等…。

還有其他各種不同的 activations 相關的說明可參考<How to activate an app>與<ActivationKind enumeration>。

 

 

[範例]

1. 情境說明,App 中有二個 Page:MainPage 與 ActivatePage;

    MainPage.xaml 需要用戶輸入登入資料後才可以進入 ActivatePage;

    ActivatePage 專用來接受 MainPage.xaml 登入後才可以顯示的內容,所以 URI scheme 要進入也要經過 MainPage.xaml;

 

2. 由於 activation action 是不會經過 OnLaunched 事件的,所以需要將 Windows.Current.Content 的建立與 Suspended 還原給獨立出來

a. App.cs 獨立成二個方法:

    CreateRootFrame():負責建立 Windows.Current.Content 需要的 rootFrame;

    RestoreStatus():負責識別 App 的上一個狀態是否為 Terminated 或 ClosedByUser 來還原狀態;

private Frame CreateRootFrame()
{
    Frame rootFrame = Window.Current.Content as Frame;

		
		
    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active
    if (rootFrame == null)
    {
        // Create a Frame to act as the navigation context and navigate to the first page
        rootFrame = new Frame();

		
		
        // TODO: change this value to a cache size that is appropriate for your application
        rootFrame.CacheSize = 1;

		
		
        // Place the frame in the current Window
        Window.Current.Content = rootFrame;
    }

		
		
    return rootFrame;
}

		
		
private async void RestoreStatus(ApplicationExecutionState previousExecutionState)
{
    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active
    if (previousExecutionState == ApplicationExecutionState.Terminated)
    {
        // Restore the saved session state only when appropriate                
    }
}

 

b. 調整 OnLaunched 的事件搭配二個方法來建立啟動 App 該做的事情:

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
#if DEBUG
    if (System.Diagnostics.Debugger.IsAttached)
    {
        this.DebugSettings.EnableFrameRateCounter = true;
    }
#endif
    // 改用 CreateRootFrame 與 RestoreStatus 來啟動 App
    Frame rootFrame = CreateRootFrame();
    RestoreStatus(e.PreviousExecutionState);           

		
		
    if (rootFrame.Content == null)
    {
#if WINDOWS_PHONE_APP
        // Removes the turnstile navigation for startup.
        if (rootFrame.ContentTransitions != null)
        {
            this.transitions = new TransitionCollection();
            foreach (var c in rootFrame.ContentTransitions)
            {
                this.transitions.Add(c);
            }
        }

		
		
        rootFrame.ContentTransitions = null;
        rootFrame.Navigated += this.RootFrame_FirstNavigated;
#endif

		
		
        // When the navigation stack isn't restored navigate to the first page,
        // configuring the new page by passing required information as a navigation
        // parameter
        if (!rootFrame.Navigate(typeof(MainPage), e.Arguments))
        {
            throw new Exception("Failed to create initial page");
        }
    }

		
		
    // Ensure the current window is active
    Window.Current.Activate();
}

此部分要注意,我未將 Windows.Current.Activate() 放到 CreateRootFrame 主要是因為讓之後如果 OnActivated 需要更換 rootFrame 中的 Page 可再使用;

 

c. 設定 OnActivated 需要加入 CreateRootFrame 與 RestoreStatus() 二個方法,並且將 URI scheme 放入指定的畫面:

protected override void OnActivated(IActivatedEventArgs args)
{
    base.OnActivated(args);

		
		
    // 識別是否為 URI scheme 的 activation event
    if (args.Kind == ActivationKind.Protocol)
    {
        ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs;
        // 取得 URI
        String uri = eventArgs.Uri.OriginalString;   

		
		
        // 加入啟動畫面的建立與資料還原
        Frame rootFrame = CreateRootFrame();
        RestoreStatus(args.PreviousExecutionState);

		
		
        // 判斷目前是否有用戶登入過
        if (AppManager.User != null)
        {
            // 代表可能存在非 MainPage 的畫面,找到對應的 Page 請求更新
   AppManager.URI = uri;
            Page page = rootFrame.Content as Page;
            if (page.GetType().Name == "ActivatedPage")
            {
                ActivatedPage act = page as ActivatedPage;
                act.UpdateTxtURI();
            }
        }
        else
        {
            // 未登入過,所以要設定進入畫面為 MainPage
            if (rootFrame.Content == null)
            {
                // 設定啟動畫面為何
                if (!rootFrame.Navigate(typeof(MainPage), uri))
                {
                    throw new Exception("Failed to create initial page");
                }
            }
            else
            {
                // 表示有畫面將資料儲存起來
                AppManager.URI = uri;
            }
        } 

		
		
        // Ensure the current window is active
        Window.Current.Activate();
    }
}

 

為什麼需要加入 CreateRootFrame 與 RestoreStatus() 二個方法?

1. App 未啟動過,用戶透過 URI scheme 來開啟 App,OnLaunched 將不會被啟動等同於沒有 Windows.Current.Content;

2. App 可能在之前被用戶關閉或系統終結,所以加入 RestoreStatus() 可以協助還原該有的資料;

 

 

d. MainPage.xaml.cs 的內容:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

		
		
    if (e.Parameter != null)
        AppManager.URI = e.Parameter.ToString();
}

		
		
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
    if (localSettings.Values.ContainsKey("id"))
    {
        txtId.Text = localSettings.Values["id"].ToString();
    }

		
		
    if (localSettings.Values.ContainsKey("pwd"))
    {
        txtPwd.Password = localSettings.Values["pwd"].ToString();
    }            
}

		
		
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
    if (String.IsNullOrEmpty(txtId.Text) || String.IsNullOrEmpty(txtPwd.Password))
    {
        MessageDialog msg = new MessageDialog("please input login data!");
        msg.ShowAsync();
    }
    else
    {
        ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
        if (localSettings.Values.ContainsKey("id"))
        {
            localSettings.Values["id"] = txtId.Text;
        }
        else
        {
            localSettings.Values.Add("id", txtId.Text);
        }

		
		
        if (localSettings.Values.ContainsKey("pwd"))
        {
            localSettings.Values["pwd"] = txtPwd.Password;
        }
        else
        {
            localSettings.Values.Add("pwd", txtPwd.Password);
        }

		
		
        AppManager.User = new UserData { Id = txtId.Text, Pwd = txtPwd.Password };

		
		
        Frame rootFrame = Window.Current.Content as Frame;
        if (rootFrame != null)
        {
            rootFrame.Navigate(typeof(ActivatedPage), "From MainPage");
        }
    }
}

		
		
private async void Button_Click(object sender, RoutedEventArgs e)
{
    Button btn = sender as Button;
    Uri uri = new Uri("bus://test_" + btn.Content.ToString());
    Boolean result = await Launcher.LaunchUriAsync(uri);
}

主要負責登入的邏輯,本身沒有帶任何有關 URI scheme 的任務,將交由 AppManager.URI 來儲存;

 

e. ActivatedPage 處理的任務,識別 AppManager.URI 是否有值,有就開發更新;

public ActivatedPage()
{
    this.InitializeComponent();

		
		
#if WINDOWS_PHONE_APP
    gdBackArea.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
#endif
#if WINDOWS_APP
    spRoot.Margin = new Thickness(0, 100, 0, 0);
#endif

		
		
    this.Loaded += ActivatedPage_Loaded;
}

		
		
void ActivatedPage_Loaded(object sender, RoutedEventArgs e)
{
    UpdateTxtURI();
}

		
		
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    if (e.Parameter != null)
    {
        txtFrom.Text = e.Parameter.ToString();
    }
}

		
		
private void backButton_Click(object sender, RoutedEventArgs e)
{
    Frame rootFarme = Window.Current.Content as Frame;
    rootFarme.GoBack();
}

		
		
public void UpdateTxtURI()
{
    txtURI.Text = AppManager.URI;
    AppManager.URI = String.Empty;
}

 

[範例程式]

 

(http://1drv.ms/1xhaPR4)

更多範例的說明可以參考<Association launching sample>。

======

學習處理 URI scheme 我最覺得重要的點在於 OnActivated 的事件。

由於 App 在 Store 與 Phone  版在 suspended 的時機不一致,所以需要考量用戶啟動 URI scheme 的時機點,

可能 App 未 suspended 或是未開啟過 App 或是已被 suspended,這樣一來有可能不會進入 OnLanuched 事件…等。

所以 App 在處理 OnActivated 事件時,需要特別注意是否需要做返回 App 的畫面重新建立與資料的還原

 

 

References:

How to handle URI activation (XAML) (重要)

Guidelines and checklist for file types and URIs

Default Programs (說明用戶在某些特殊條件(URIs,File extensions)預設開啟的 App)

Association launching sample

How to launch the default app for a protocol (C#/VB/C++)

Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs

Windows.UI.WebUI.WebUIProtocolActivatedEventArgs

App contracts and extensions (Windows Runtime apps) (重要)

Application (相關 package.appxmanifest 的設定說明)

ActivationKind enumeration

 

Dotblogs Tags: ,